برنامه نویسی ۶۷۱ بازدید

ایجاد یک پکیج لاراول روشی عالی برای جلوگیری از تکرار کردن کدها محسوب می‌شود. این مقصود در صورتی که از یک الگوی طراحی ریپازیتوری نیز بهره بگیرید بسیار بهینه‌تر خواهد بود و موجب می‌شود که کنترلرها منظم مانده و برای تست آماده باشند.

بدین ترتیب در پروژه‌های جدید می‌توان دقیقاً از همان کد ریپازیتوری قبلی استفاده کرد. همزمان مسئله کپی کردن و چسباندن کد بین پروژه‌ها نیز منتفی خواهد بود. راه‌حل این است که کد ریپازیتوری را به پکیج لاراول خودش منتقل کنیم. در ادامه با بهره‌گیری از کامپوزر و همچنین منطق بارگذاری خودکار می‌توانیم کلاس‌های خود را در پروژه بارگذاری کرده و به دستور make:repository خود دسترسی پیدا کنیم.

ساختار پروژه‌ای که می‌خواهیم داشته باشیم به صورت زیر است:

lazyelephpant/ 
    repository/ 
        src/
            Console/ 
                Command/ 
                    Stubs/

بدین ترتیب می‌توان با اجرای دستور composer init به صورت تعاملی یک فایل composer.json ساخت.

با این حال، ما در این جا یک فایل composer.json قدیمی را از پروژه دیگری کپی کرده‌ایم و مقادیر آن را تغییر داده‌ایم.

{
  "name": "lazyelephpant/repository",
  "description": "Laravel package to pull in repository functionality across multiple projects.",
  "type": "library",
  "authors": [
    {
      "name": "Kevin Pimentel",
      "email": "kevin@kevinpimentel.com"
    }
  ],
  "require": {},
  "require-dev": {},
  "autoload": {
    "psr-4": {
      "LazyElePHPant\\Repository\\": "src/"
    }
  },
  "extra": {
    "laravel": {
      "providers": [
        "LazyElePHPant\\Repository\\RepositoryServiceProvider"
      ],
      "aliases": {}
    }
  }
}

بارگذاری خودکار

نخستین گام مهم بحث «بارگذاری خودکار» (Autoloading) است.

"autoload": {
  "psr-4": {
    "LazyElePHPant\\Repository\\": "src/"
  }
}

اگر با طرز کار جادویی بارگذاری خودکار آشنا نیستید باید بگوییم که کامپوزر یک autoloader به صورت PSR-4 برای فضای نام LazyElePHPant ثبت می‌کند که اساساً رشته LazyElePHPant را به src/folder نگاشت می‌کند.

در ادامه باید فایل autoload.php را در مسیر پوشه vendor/ مجدداً ایجاد بکنید. این کار از طریق اجرای دستور dump-autoload میسر است.

کشف پکیج لاراول

در لاراول می‌توان پکیج‌هایی تنظیم کرد که فریمورک‌ها با مراجعه به مسیر config/app.php آن‌ها را کشف کرده و به آرایه provider-ها اضافه کنند. با این وجود، ما می‌خواهیم پکیجمان این کار را به صورت خودکار انجام دهد:

"extra": {
  "laravel": {
    "providers": [
      "LazyElePHPant\\Repository\\RepositoryServiceProvider"
    ],
    "aliases": {}
  }
}

با تعیین مقدار RepositoryServiceProvider در بخش extras فایلِ composer.json می‌توانیم کاری کنیم که لاراول ارائه‌دهنده سرویس را به صورت خودکار بارگذاری کند.

باید فایل RepositoryInterface و پیاده‌سازی Repository را در پوشه src/ قرار دهیم. این فایل‌ها در حال حاضر از سوی پروژه جاری در حال استفاده هستند. آن‌ها را در ادامه طوری ویرایش کرده‌ایم که از درون پکیج لاراول نیز کار کنند.

فایل RepositoryInterface.php

<?php
namespace LazyElePHPant\Repository;
use Illuminate\Database\Eloquent\Model;
interface RepositoryInterface
{
    public function all($columns = ['*']);
    public function list($orderByColumn, $orderBy = 'desc', $with = [], $columns = ['*']);
    public function create(array $data);
    public function update(array $data, $id);
    public function delete($id);
    public function find($id, $columns = array('*'));
    public function findBy(string $field, mixed $value, $columns = array('*'));
    public function setModel(Model $model);
    public function getModel();
}

فایل Repository.php

<?php
namespace LazyElePHPant\Repository;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Container\Container as App;
abstract class Repository implements RepositoryInterface
{
    /** @var App */
    private $app;
    /** @var Illuminate\Database\Eloquent\Model */
    private $model;
    public function __construct(App $app)
    {
        $this->app = $app;
        $this->makeModel();
    }
    abstract public function model();
    public function makeModel()
    {
        $model = $this->app->make($this->model());
        if (!$model instanceof Model) {
            throw new RepositoryException(
                "Class {$this->model()} must be an instance of Illuminate\\Database\\Eloquent\\Model"
            );
        }
        return $this->model = $model;
    }
    public function all($columns = array('*'))
    {
        return $this->model->get($columns);
    }
    public function list($orderByColumn, $orderBy = 'desc', $with = [], $columns = ['*'])
    {
        return $this->model->with($with)
                           ->orderBy($orderByColumn, $orderBy)
                           ->get($columns);
    }
    public function create(array $data)
    {
        return $this->model->create($data);
    }
    public function update(array $data, $id, $attribute = 'id')
    {
        return $this->model->where($attribute, '=', $id)->update($data);
    }
    public function delete($id)
    {
        return $this->model->destroy($id);
    }
    public function find($id, $columns = array('*'))
    {
        return $this->model->find($id, $columns);
    }
    public function findBy(string $field, mixed $value, $columns = array('*'))
    {
        return $this->model->where($field, $value)
                           ->select($columns)
                           ->first();
    }
    public function setModel(Model $model)
    {
        $this->model = $model;
    }
    public function getModel()
    {
        return $this->model;
    }
}

در این مرحله پوشه پکیج لاراول، ساختاری مانند زیر دارد:

lazyelephpant/
    repository/
        composer.json
        src/
            RepositoryInterface.php
            Repository.php
            Console/
	           Command/
		          Stubs/

برای این که بدانید معنی stub چیست، باید به صورت عمیق‌تری این فریمورک را بررسی کنیم. در مستندات لاراول اشاره‌هایی به stub وجود دارد، اما توضیح جامعی در مورد آن‌ها ارائه نشد است. stub-ها برای اغلب کسانی که سروکار عمیقی با لاراول ندارند موضوع جدیدی محسوب می‌شوند و باید با فریمورک سروکله بزنید تا بفهمید که php artisan چگونه می‌تواند کنترلر و کلاس‌های مدل را ایجاد کند. پاسخ این سؤال در stub نهفته است. اگر تاکنون از دستورهای زیر استفاده کرده باشید:

php artisan make:controller SomeController

یا

php artisan make:model SomeModel

می‌دانید که stub یک قالب است که این کلاس‌ها بر مبنای آن ساخته شده‌اند. ما با نگاه به طرز کار فریمورک، stub-های خودمان را ساختیم که به صورت زیر هستند:

فایل repository.plain.stub

namespace DummyNamespace;

use LazyElePHPant\Repository\Repository;
use Illuminate\Database\Eloquent\Model;

class DummyClass extends Repository
{
    public function model()
    {
        return Model::class;
    }
}

Stub ساده، قالبی است که در زمان ساخت مدل تعیین می‌شود:

php artisan make:repository

با این حال اگر می‌خواهید یک مدل تعیین کنید، باید از قالبی به نام repository.stub استفاده کنید:

php artisan make:repository --model=User

فایل repository.stub

namespace DummyNamespace;

use LazyElePHPant\Repository\Repository;
use NamespacedDummyModel;

class DummyClass extends Repository
{
    public function model()
    {
        return DummyModel::class;
    }
}

ایده کار این است که یک ریپازیتوری با مدل User تولید می‌کنیم:

namespace App\Repository;

use LazyElePHPant\Repository\Repository;
use App\User;

class UserClass extends Repository
{
    public function model()
    {
        return User::class;
    }
}

دستور Repository

در نهایت به دستوری می‌رسیم که همه کارها را بر عهده دارد.

فایل RepositoryMakeCommand.php

<?php
namespace LazyElePHPant\Repository\Console\Commands;
use Illuminate\Support\Str;
use Illuminate\Console\GeneratorCommand;
use Symfony\Component\Console\Input\InputOption;
class RepositoryMakeCommand extends GeneratorCommand
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $name = 'make:repository';
    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Create a new repository class';
    /**
     * Build the class with the given name.
     *
     * @param  string  $name
     * @return string
     */
    protected function buildClass($name)
    {
        $stub = parent::buildClass($name);
        $model = $this->option('model');
        return $model ? $this->replaceModel($stub, $model) : $stub;
    }
    /**
     * Replace the model for the given stub.
     *
     * @param  string  $stub
     * @param  string  $model
     * @return string
     */
    protected function replaceModel($stub, $model)
    {
        $model = str_replace('/', '\\', $model);
        $namespaceModel = $this->laravel->getNamespace().$model;
        $stub = (Str::startsWith($model, '\\'))
              ? str_replace('NamespacedDummyModel', trim($model, '\\'), $stub)
              : str_replace('NamespacedDummyModel', $namespaceModel, $stub);
        $stub = str_replace(
            "use {$namespaceModel};\nuse {$namespaceModel};", "use {$namespaceModel};", $stub
        );
        $model = class_basename(trim($model, '\\'));
        $dummyModel = Str::camel($model) === 'user' ? 'model' : $model;
        return str_replace('DummyModel', $model, $stub);
    }
    /**
     * Get the stub file for the generator.
     *
     * @return string
     */
    protected function getStub()
    {
        return $this->option('model')
                    ? __DIR__.'/Stubs/repository.stub'
                    : __DIR__.'/Stubs/repository.plain.stub';
    }
    /**
    * Get the default namespace for the class.
    *
    * @param  string  $rootNamespace
    * @return string
    */
    protected function getDefaultNamespace($rootNamespace)
    {
        return $rootNamespace . '\Repository';
    }
    /**
    * Get the console command options.
    *
    * @return array
    */
    protected function getOptions()
    {
        return [
            ['model', 'm', InputOption::VALUE_OPTIONAL, 'The model that the repository applies to']
        ];
    }
}

اینک که می‌دانیم بخش‌های مختلف چه هستند. ساختار پکیج به صورت زیر خواهد بود:

lazyelephpant/
    repository/
        composer.json
        src/
            RepositoryInterface.php
            Repository.php
            Console/
	           Command/
                    RepositoryMakeCommand.php
    		           Stubs/
                        repository.stub
                        repository.plain.stub

در این مرحله ساختار پکیج لاراول ما به صورت زیر است. تنها یک مشکل وجود دارد. مهم‌ترین بخش را که کلاس RepositoryServiceProvider.php است، فراموش کرده‌ایم.

ارائه‌دهنده سرویس پکیج لاراول

باید یک کلاس RepositoryServiceProvider.php را در پوشه src/ ایجاد کنیم. RepositoryServiceProvider باید کلاس ServiceProvider.php لاراول را بسط دهد.

<?php

namespace LazyElePHPant\Repository;

use Illuminate\Support\ServiceProvider;

class RepositoryServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot() {}

    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
    	
    }
}

در مستندات (+) نیز به روش دقیق ثبت دستور اشاره شده است:

<?php

namespace LazyElePHPant\Repository;

use Illuminate\Support\ServiceProvider;
use LazyElePHPant\Repository\Console\Commands\RepositoryMakeCommand;

class RepositoryServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
    	if ($this->app->runningInConsole()) {
	        $this->commands([
	            RepositoryMakeCommand::class,
	        ]);
	    }
    }

    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {

    }
}

اینک همه چیز تنظیم شده است و آماده تست هستیم. برای تست لوکال پکیج لاراول به فایل کامپوزر خود بازمی‌گردیم و یک مدخل برای الزام (required) آن اضافه می‌کنیم:

"lazyelephpant/repository": "dev-master",
...
"repositories":[
        {
            "type":"path",
            "url":"../lazyelephpant/repository",
            "options":{
                "symlink":true
            }
        }
]

سپس دستور زیر را اجرا می‌کنیم تا پکیج دریافت شود:

composer update

الگوی طراحی ریپازیتوری

اینک پکیج در پوشه vendor قرار گرفته است و می‌توانیم بررسی کنیم آیا دستور کار می‌کند:

الگوی طراحی ریپازیتوری

بدین ترتیب کلاس ریپازیتوری با موفقیت بدون مدل ساخته می‌شود:

<?php

namespace App\Repository;

use LazyElePHPant\Repository\Repository;
use Illuminate\Database\Eloquent\Model;

class TestRepository extends Repository
{
    public function model()
    {
        return Model::class;
    }
}

اکنون بررسی می‌کنیم که امکان انتساب یک مدل را داریم یا نه:

الگوی طراحی ریپازیتوری

<?php

namespace App\Repository;

use LazyElePHPant\Repository\Repository;
use App\User;

class TestRepository extends Repository
{
    public function model()
    {
        return User::class;
    }
}

در ادامه یک dump ساده را می‌بینید که نشان می‌دهد در عمل TestRepository را بازیابی کرده‌ایم:

use App\Repository\TestRepository;

...

public function __construct(TestRepository $testRepository)
{
    dd($testRepository);
}

الگوی طراحی ریپازیتوری

سخن پایانی

برخی از مخالفان برنامه‌نویسی شیءگرا استدلال می‌کنند که در نهایت وقتی لازم باشد از کدی در پروژه دیگر استفاده کنید، باید کد را به آن پروژه کپی کنید. امروزه اغلب زبان‌های برنامه‌نویسی از نوعی سیستم مدیریت پکیج بهره می‌گیرند. به نظر می‌رسد که ضرورت ایجاد کتابخانه‌هایی با قابلیت استفاده مجدد، اینک بیش از هر زمان دیگری حس می‌شود. اگر حس می‌کنید لازم است کدی را از پروژه‌ای به پروژه دیگر کپی کنید، شاید بهتر باشد یک پکیج برای خود بسازید.

اگر این نوشته برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۱ نفر
آیا این مطلب برای شما مفید بود؟
شما قبلا رای داده‌اید!
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.

«میثم لطفی» در رشته‌های ریاضیات کاربردی و مهندسی کامپیوتر به تحصیل پرداخته و شیفته فناوری است. وی در حال حاضر علاوه بر پیگیری علاقه‌مندی‌هایش در رشته‌های برنامه‌نویسی، کپی‌رایتینگ و محتوای چندرسانه‌ای، در زمینه نگارش مقالاتی با محوریت نرم‌افزار با مجله فرادرس همکاری دارد.