الگوی طراحی ریپازیتوری برای پکیج های لاراول — راهنمای کاربردی

۱۲۵ بازدید
آخرین به‌روزرسانی: ۱۵ مهر ۱۴۰۲
زمان مطالعه: ۶ دقیقه
الگوی طراحی ریپازیتوری برای پکیج های لاراول — راهنمای کاربردی

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

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

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

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

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

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

1{
2  "name": "lazyelephpant/repository",
3  "description": "Laravel package to pull in repository functionality across multiple projects.",
4  "type": "library",
5  "authors": [
6    {
7      "name": "Kevin Pimentel",
8      "email": "kevin@kevinpimentel.com"
9    }
10  ],
11  "require": {},
12  "require-dev": {},
13  "autoload": {
14    "psr-4": {
15      "LazyElePHPant\\Repository\\": "src/"
16    }
17  },
18  "extra": {
19    "laravel": {
20      "providers": [
21        "LazyElePHPant\\Repository\\RepositoryServiceProvider"
22      ],
23      "aliases": {}
24    }
25  }
26}

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

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

کدهای مربوطه را در ادامه آورده‌ایم.

1"autoload": {
2  "psr-4": {
3    "LazyElePHPant\\Repository\\": "src/"
4  }
5}

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

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

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

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

1"extra": {
2  "laravel": {
3    "providers": [
4      "LazyElePHPant\\Repository\\RepositoryServiceProvider"
5    ],
6    "aliases": {}
7  }
8}

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

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

فایل RepositoryInterface.php

1<?php
2namespace LazyElePHPant\Repository;
3use Illuminate\Database\Eloquent\Model;
4interface RepositoryInterface
5{
6    public function all($columns = ['*']);
7    public function list($orderByColumn, $orderBy = 'desc', $with = [], $columns = ['*']);
8    public function create(array $data);
9    public function update(array $data, $id);
10    public function delete($id);
11    public function find($id, $columns = array('*'));
12    public function findBy(string $field, mixed $value, $columns = array('*'));
13    public function setModel(Model $model);
14    public function getModel();
15}

فایل Repository.php

1<?php
2namespace LazyElePHPant\Repository;
3use Illuminate\Database\Eloquent\Model;
4use Illuminate\Container\Container as App;
5abstract class Repository implements RepositoryInterface
6{
7    /** @var App */
8    private $app;
9    /** @var Illuminate\Database\Eloquent\Model */
10    private $model;
11    public function __construct(App $app)
12    {
13        $this->app = $app;
14        $this->makeModel();
15    }
16    abstract public function model();
17    public function makeModel()
18    {
19        $model = $this->app->make($this->model());
20        if (!$model instanceof Model) {
21            throw new RepositoryException(
22                "Class {$this->model()} must be an instance of Illuminate\\Database\\Eloquent\\Model"
23            );
24        }
25        return $this->model = $model;
26    }
27    public function all($columns = array('*'))
28    {
29        return $this->model->get($columns);
30    }
31    public function list($orderByColumn, $orderBy = 'desc', $with = [], $columns = ['*'])
32    {
33        return $this->model->with($with)
34                           ->orderBy($orderByColumn, $orderBy)
35                           ->get($columns);
36    }
37    public function create(array $data)
38    {
39        return $this->model->create($data);
40    }
41    public function update(array $data, $id, $attribute = 'id')
42    {
43        return $this->model->where($attribute, '=', $id)->update($data);
44    }
45    public function delete($id)
46    {
47        return $this->model->destroy($id);
48    }
49    public function find($id, $columns = array('*'))
50    {
51        return $this->model->find($id, $columns);
52    }
53    public function findBy(string $field, mixed $value, $columns = array('*'))
54    {
55        return $this->model->where($field, $value)
56                           ->select($columns)
57                           ->first();
58    }
59    public function setModel(Model $model)
60    {
61        $this->model = $model;
62    }
63    public function getModel()
64    {
65        return $this->model;
66    }
67}

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

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

1namespace DummyNamespace;
2
3use LazyElePHPant\Repository\Repository;
4use Illuminate\Database\Eloquent\Model;
5
6class DummyClass extends Repository
7{
8    public function model()
9    {
10        return Model::class;
11    }
12}

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

php artisan make:repository

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

php artisan make:repository --model=User

فایل repository.stub

1namespace DummyNamespace;
2
3use LazyElePHPant\Repository\Repository;
4use NamespacedDummyModel;
5
6class DummyClass extends Repository
7{
8    public function model()
9    {
10        return DummyModel::class;
11    }
12}

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

1namespace App\Repository;
2
3use LazyElePHPant\Repository\Repository;
4use App\User;
5
6class UserClass extends Repository
7{
8    public function model()
9    {
10        return User::class;
11    }
12}

دستور Repository

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

فایل RepositoryMakeCommand.php

1<?php
2namespace LazyElePHPant\Repository\Console\Commands;
3use Illuminate\Support\Str;
4use Illuminate\Console\GeneratorCommand;
5use Symfony\Component\Console\Input\InputOption;
6class RepositoryMakeCommand extends GeneratorCommand
7{
8    /**
9     * The name and signature of the console command.
10     *
11     * @var string
12     */
13    protected $name = 'make:repository';
14    /**
15     * The console command description.
16     *
17     * @var string
18     */
19    protected $description = 'Create a new repository class';
20    /**
21     * Build the class with the given name.
22     *
23     * @param  string  $name
24     * @return string
25     */
26    protected function buildClass($name)
27    {
28        $stub = parent::buildClass($name);
29        $model = $this->option('model');
30        return $model ? $this->replaceModel($stub, $model) : $stub;
31    }
32    /**
33     * Replace the model for the given stub.
34     *
35     * @param  string  $stub
36     * @param  string  $model
37     * @return string
38     */
39    protected function replaceModel($stub, $model)
40    {
41        $model = str_replace('/', '\\', $model);
42        $namespaceModel = $this->laravel->getNamespace().$model;
43        $stub = (Str::startsWith($model, '\\'))
44              ? str_replace('NamespacedDummyModel', trim($model, '\\'), $stub)
45              : str_replace('NamespacedDummyModel', $namespaceModel, $stub);
46        $stub = str_replace(
47            "use {$namespaceModel};\nuse {$namespaceModel};", "use {$namespaceModel};", $stub
48        );
49        $model = class_basename(trim($model, '\\'));
50        $dummyModel = Str::camel($model) === 'user' ? 'model' : $model;
51        return str_replace('DummyModel', $model, $stub);
52    }
53    /**
54     * Get the stub file for the generator.
55     *
56     * @return string
57     */
58    protected function getStub()
59    {
60        return $this->option('model')
61                    ? __DIR__.'/Stubs/repository.stub'
62                    : __DIR__.'/Stubs/repository.plain.stub';
63    }
64    /**
65    * Get the default namespace for the class.
66    *
67    * @param  string  $rootNamespace
68    * @return string
69    */
70    protected function getDefaultNamespace($rootNamespace)
71    {
72        return $rootNamespace . '\Repository';
73    }
74    /**
75    * Get the console command options.
76    *
77    * @return array
78    */
79    protected function getOptions()
80    {
81        return [
82            ['model', 'm', InputOption::VALUE_OPTIONAL, 'The model that the repository applies to']
83        ];
84    }
85}

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

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 لاراول را بسط دهد.

1<?php
2
3namespace LazyElePHPant\Repository;
4
5use Illuminate\Support\ServiceProvider;
6
7class RepositoryServiceProvider extends ServiceProvider
8{
9    /**
10     * Bootstrap services.
11     *
12     * @return void
13     */
14    public function boot() {}
15
16    /**
17     * Register services.
18     *
19     * @return void
20     */
21    public function register()
22    {
23    	
24    }
25}

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

1<?php
2
3namespace LazyElePHPant\Repository;
4
5use Illuminate\Support\ServiceProvider;
6use LazyElePHPant\Repository\Console\Commands\RepositoryMakeCommand;
7
8class RepositoryServiceProvider extends ServiceProvider
9{
10    /**
11     * Bootstrap services.
12     *
13     * @return void
14     */
15    public function boot()
16    {
17    	if ($this->app->runningInConsole()) {
18	        $this->commands([
19	            RepositoryMakeCommand::class,
20	        ]);
21	    }
22    }
23
24    /**
25     * Register services.
26     *
27     * @return void
28     */
29    public function register()
30    {
31
32    }
33}

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

1"lazyelephpant/repository": "dev-master",
2...
3"repositories":[
4        {
5            "type":"path",
6            "url":"../lazyelephpant/repository",
7            "options":{
8                "symlink":true
9            }
10        }
11]

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

composer update

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

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

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

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

1<?php
2
3namespace App\Repository;
4
5use LazyElePHPant\Repository\Repository;
6use Illuminate\Database\Eloquent\Model;
7
8class TestRepository extends Repository
9{
10    public function model()
11    {
12        return Model::class;
13    }
14}

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

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

1<?php
2
3namespace App\Repository;
4
5use LazyElePHPant\Repository\Repository;
6use App\User;
7
8class TestRepository extends Repository
9{
10    public function model()
11    {
12        return User::class;
13    }
14}

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

1use App\Repository\TestRepository;
2
3...
4
5public function __construct(TestRepository $testRepository)
6{
7    dd($testRepository);
8}

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

سخن پایانی

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

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

==

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

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *