نحوه ساخت Dynamic منو با استفاده از recursive function


توی این آموزش میخوام نحوه ساخت یک دینامیک منو رو آموزش بدم که بتوانیم با خواندن اطلاعات از یک table ارتباط بین منوها را تشخیص و سپس منو را sort و ایجاد کرده. اینکارو با استفاده از laravel میخوام انجام بدم .

#مراحل انجام کار 

1- ساخت یک table به نام Menu

2- ساخت یک Abstraction Class به نام Imenu

3- ساخت یک Concrete class به نام VerticalMenu

#ساخت یک table به نام Menu

در ابتدا یک table میخواهیم که این table مشخص کننده منوهایی هستند که میخواهیم آنها را در ظاهر نمایش بدیم . table که من ساختم شامل اطلاعات زیر میباشد .

Schema::create('menu', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('parent_id');
            $table->string('title');
            $table->string('link');
            $table->string('img');
        });

خوب بعد از اینکه table مورد نظر را ایجاد کردیم، باید Abstraction Class خود را بسازیم.

#ساخت یک Abstraction Class به نام Imenu

abstract class IMenu
{

    protected $table;

    protected $class_name;

    protected $level;


    /**
     * @param array $selected_tags
     * @return mixed
     */
    public function render(){

        $parents = $this->table->where("parent_id", 0)->get()->pluck("id", "id");
        return $this->create($parents);
    }

    /**
     * @param $parents
     * @return mixed
     */
    abstract protected function create($parents);

    /**
     * using this method to set a class selector for the html ul tag
     * @param $class_name
     * @return $this
     */
    public function setClassName($class_name){
        $this->class_name = $class_name;
        return $this;
    }

    /**
     * get Li title|name|... using table row id and the field name
     * @param $row_id
     * @param $field_name
     * @return mixed
     */
    protected function getLiTitle($row_id, $field_name){

        return $this->table->whereId($row_id)->first()->{$field_name};
    }

    /**
     * @param $table
     * @return $this
     */
    public function setTable($table){
        $this->table = $table;
        return $this;
    }

}

متد render جهت بدست آوردن تمامی رکوردهایی میباشد که parent_id آنها 0 میباشد ، این یعنی اینکه تمامی رکوردهای والد , سپس از متد pluck استفاده شده است.

از متد pluck برای ایجاد یک collection از ستونهای موجود در هر آبجکت یک collection استفاده میشود که فقط 2 ستون رو میتواند به عنوان آرگومان ورودی دریافت کند .

سپس متد create صدا زده شده است ، این یعنی اینکه ما اگه بخوایم منو خودمون رو بسازیم به صورت مستقیم نمیتونیم از متد create استفاده کنیم و باید متد render را صدا بزنیم .

setClassName برای ست کردن نام کلاسی میباشد که در هنگام خروجی منو بخواهیم به عنوان css selector روی آن عملیاتی انجام دهیم .

getLiTitle جهت بدست آوردن نام یک ستون با شماره سطر ارسال شده میباشد .

setTable جهت ست کردن model مورد نظر به عنوان یک خصوصیت میباشد، که در صورت نیاز به table از خصوصیت table در abstract کلاس استفاده کنیم .

دلیل استفاده از Abstract Class یا Interface چیه ؟ 

  • یکی از دلایل استفاده از Abstract class، مجبور کردن کاربر به استفاده از متدهایی میباشد که در Abstract class تعریف کرده باشیم .
  • دلیل دوم به خاطر اینه که ما هیچ موقع از Concrete کلاس مطمئن نیستیم و همیشه به کلاسهایی که Interface یا Abstraction که ما ایجاد کرده باشیم و Implement کرده باشن اعتماد میکنیم . اینکار سبب از بین رفتن decoupling میشود و دیگه سیستم شما وابسته به Concrete کلاس نیست (که ممکن فردا تغییرات زیادی توی این کلاس انجام بشه) بلکه وابسته به interface ثابتی میباشد که فقط مخصوص انجام دادن یک کار میباشد.

#ساخت یک Concrete class به نام VerticalMenu

توی این کلاس ما باید منو خودمون رو بسازیم لذا باید در ابتدا از تابع create که در کلاس abstract ساختیم استفاده کنیم و منو خودمون رو بسازیم .

class VerticalMenu extends IMenu
{

    protected function create($parents){

        $html = "";
        $html .= "<ul class='".$this->class_name."'>";
        foreach( $parents as $key => $value ){

            if( $this->table->where("parent_id", $value)->count() > 0 ){

                $html .= "<li> <div class='".$this->class_name."_li'> <a class='".$this->class_name."_name' href='#'> ".$this->level.$this->getLiTitle($value, "name")."</a></div>";
                $html .= $this->create($this->table->where("parent_id", $value)->get()->pluck("id", "id"));
                $html .= "</li>";

            } else {
                $html .= "<li> <div class='".$this->class_name."_li'> <a class='".$this->class_name."_name' href='#'> ".$this->level.$this->getLiTitle($value, "name")." </a> </div> </li>";
            }
        }

        $this->ascendLevel();
        $html .= "</ul>";

        return $html;
    }

}

اتفاقاتی که در کلاس بالا رخ میدهد را به صورت پله پله میگم . در ابتدا همه تگها را در یک متغیر به نام html ذخیره میکنیم . همه تگها هم در یک ul کلی ذخیره میشوند .

  1. متد create از کلاس verticalMenu یک آرایه را دریافت میکند که این آرایه حاوی تمامی رکوردهایی میباشد که parent_id آنها 0 باشد
  2. سپس با استفاده از یک حلقه  تمامی اعضای این collection را بررسی میکنم .
  3. شرط اول که در حلقه foreach وجود دارد، بررسی میکند که آیا این رکورد فرزندان دیگری هم دارد یا نه . در صورتی که این رکورد فرزندان دیگری داشته باشد اولین شرط اجرا میشود .
  4. در صورت وجود داشتن فرزندان دیگر ، در ابتدا یک تگ li را ایجاد کردیم که این تگ اطلاعاتی مثل نام ستون که در حال حاضر در حال پیمایش آن هستیم و همچنین نام کلاسی که میخواهیم از آن به عنوان یک css-selector استفاده کنیم را استفاده میکنیم.
  5. سپس به خصوصیت level یک خط تیره اضافه کردیم، به این دلیل که ما با استفاده از خط تیره میتوانیم مشخص کنیم که تگ li یک تگ فرزند میباشد .
  6. سپس تمامی رکوردهایی که رکورد فعلی به عنوان والد آنها میباشد را select  میکنیم و به صورت یک collection آن را به متد create ارسال میکنیم .
  7. این چرخه آنقدر ادامه پیدا میکند تا به آخرین فرزند موجود برسد ، در صورت رسیدن به آخرین فرزند متد ascendLevel صدا زده میشود که ، این متد یک خط تیره را حذف میکند به دلیل اینکه تابع بازگشتی یک مرحله به قبل باز میگردد تا فرزندان دیگر رکورد بعدی را پیمایش کند .
  8. بعد از تمام شدن پیامایش همه تگها تگ ul بسته میشود و سپس با return همه مقدار موجود در متغیر html را برمیگردانیم .

به این حالت صدا زدن تابع یا متد که بر اساس شروطی دوباره خود تابع یا متد صدا زده میشود ، به عنوان recursive function معروف است. از این نوع صدا زدن خیلی باید مراقب باشید ، چرا که در صورت اشتباه شدن عملیات همانند یک حلقه while عمل میکند و از چرخه خارج نمیشود .

در کد بالا 1 متد را صدا زدیم که آن را در کد زیر نمایش میدهیم .

 protected function ascendLevel(){
        $level = explode(" ", $this->level);
        $level = array_splice($level, 0, count($level)-2);
        $this->level = implode(" ", $level);
}

در کد بالا متد ascendLevel جهت حذف یک خط تیره از کل خط تیره های موجود در خصوصیت level میباشد .

 نحوه استفاده از این کلاس به این شکل میباشد .

    /**
     * @var VerticalMenu | string
     */
    private $verticalMenu;

    /**
     * @var menu | object
     */
    private $menu;

    /**
     * @param Request $request
     * @param VerticalMenu $verticalMenu
     * @param Tag $tag
     */
    public function __construct(Imenu $verticalMenu, Menu $menu){
        $this->verticalMenu = $verticalMenu;
        $this->menu = $menu;
    }

    /**
     * Show tag index page
     *
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    public function getIndex(){

        $menu = $this->verticalMenu
            ->setTable($this->menu)
            ->setClassName("menu")
            ->render();

       return View("admin.tag.index", compact("menu", "tags"));
    }

در کد بالا اولین آرگومان ورودی constructor از نوع Imenu میباشد . واسه ما فرقی نمیکنه که چه کلاسی به این constructor ارسال بشه فقط واسه ما این مهمه که کلاسی که inject بشه از نوع abstract کلاس  Imenu  باشه . چون ما مطمئنیم که کلاسی که از این نوع باشه یک متد به نام create داره . به این شکل در صورتی که بخوایم نام کلاس رو عوض کنیم خیلی راحت میتونیم توی خود constructor عوضش کنیم ولی  verticalMenu همچنان همون نام کلاس باقی بمونه . 

تنها کار دیگه که باید انجام بدیم استفاده از container لاراول میباشد .

 $this->app->bind('App\Acme\Interfaces\IMenu', "VerticalMenu");

با این کد مشخص کردیم که کلاس verticalMenu کلاس Imenu را به عنوان یک abstract کلاس استفاده کرده لذا در صورتی که به VerticalMenu نیاز داشته باشیم لاراول به صورت اتوماتیک کلاس VerticalMenu را استفاده میکند .

نتیجه خروجی کد بالا به شکل زیر میباشد .

امیدوارم از این آموزش خوشتون اومده باشه . هر اشکال یا نظری داشتید حتما بگید .

مشکل ، مورد ، نظر ، ایده هر سوالی که به ذهنتون در مورد این آموزش میرسه لطفاً بیان کنید