imanghafoori/laravel-widgetize

A minimal yet powerful package to give a better structure and caching opportunity for your laravel apps.


Keywords
php, laravel, laravel5, laravel-cache, laravel5-package, design-pattern, html-minification, html-minifier, laravel-5-package, laravel-optimization, laravel-presenter, laravel-utility, presenter
License
MIT

Documentation

Laravel Widgetize

untitled2

Quality Score Latest Stable Version Build Status License Awesome Laravel

🎀🎀🎀 Widget Objects help you have "cleaner code" "easy caching" 🎀🎀🎀

This page may look long and boring to read at first, but bear with me!!!

I bet if you read through it you won't get disappointed at the end.So let's Go... 🏇

🔦 Introduction

This package helps you in :

  • Page Partial Caching
  • Code Organization
  • HTML Minification
  • Showing Debug information
  • Extends the laravel Router

What is a widget?

You can think of a widget as a page partial with a 'View Composer' attached to it.

Or If you know Drupal's Views concept, they are very similar to each other.

In fact a Widget is are normal php class without any magical methods, when you pass them into the render_widget() helper function or @widget() directive they magically output HTML!!! Which is the result of rendering a view partial with data from the widget controller. So we can replace @include('myPartial') with @widget('myWidget') in our laravel applications. The main benefit you get here is the fact that widget objects are cached and they know how to provide data from them selves.

When to use the widget concept?

The simple answer is : Always This concept (this design pattern) really shines when you want to create crowded web pages with multiple widgets (on sidebar, menu, carousels ...) and each widget needs separate sql queries and php logic to be provided with data for its template. Anyway installing it has minimal overhead since surprisingly it is just a small abstract class and Of course you can use it to refactor your monster code and tame it into managable pieces or boost the performance 4x-5x times faster! 💫

💎 Package Features 💎

  1. It optionally caches the output of each widget. (which give a very powerful, flexible and easy to use caching opportunity) You can set different cache config for each part of the page. Similar to ESI standard.
  2. It optionally minifies the output of the widget.
  3. It shows Debug info for your widgets as html title="" attributes.
  4. php artisan make:widget command
  5. It helps you to have a dedicated presenter class of each widget to clean up your views.
  6. It extends the Route facade with Route::view , Route::jsonWidget , Route::widget

🔧 Installation: ⬇️

composer require imanghafoori/laravel-widgetize

🔌 Next, you must install the service provider to config/app.php: 🔌

'providers' => [
    // ...
    Imanghafoori\Widgets\WidgetsServiceProvider::class,
];

Publish your config file

php artisan vendor:publish

🔥 And you will be on fire!🔥

php artisan make:widget MySexyWidget

A lot of docs are included in the generated widget file so it is not needed to memorize or even read the rest of this page. You can jump right-in and start using it.

💡 Example

How to make a Widget?

You can use : php artisan make:widget MyWidget to make your widget class.

Sample widget class :

namespace App\Widgets;

class MyWidget
{
    // The data returned here would be available in widget view file automatically.
    public function data($my_param=5)
    {
        // It's the perfect place to query the database for your widget...
        return Product::orderBy('id', 'desc')->take($my_param)->get();

    }
}

App\Widgets\MyWidgetView.blade.php :

<ul>
  @foreach($data as $product)
    <li>
      {{ $product->title }}
    </li>
  @endforeach
  
  Note that it is perfectly ok to use an other widget here 
  @widget('AnOtherWidget')
</ul>

Ok, Now it's done! We have a ready to use widget. let's use it...

How to use a widget class?

In a normal day to day view (middle-end):

<html>
    <head></head>
    <body>
        <h1>Hello {{ auth()->user()->username }} </h1> <!-- not cached -->

        @widget('RecentProductsWidget') <!-- Here we send request to back-end to get HTML -->
        
    <body>
</html>

An other way to think of widgets :

All of us, more or less have some ajax experience. One scenario is to lazy load a page partial after
the page has been fully loaded. (like jQuery Pjax plug-in does)
You can think of @widget() as an ajax call from "middle-end" to the "back-end" to load a piece of HTML
into the main page.
In facts your widgets are the 'Back-end' and your typical views are the 'middle-end' !!!

🌍 Global Config:

You can set the variables in "config/widgetize.php" file to globally set some configs for you widgets and override them per widget if needed. Read the docblocks in config/widgetize.php file for more info.

🚙 Per Widget Config:

public $template (string)

If you do not set it,By default, it refers to app/Widgets folder and looks for the 'widgetNameView.blade.php' (Meaning that if your widget is app/Widgets/home/recentProducts.php the default view for that is app/Widgets/home/recentProductsView.blade.php) Anyway you can override it to point to any partial in views folder.(For example: public $template='home.footer' will look for resource/views/home/footer.blade.php) So the entire widget lives in one folder:

| app\Widgets\Homepage\RecentProductsWidget.php

| app\Widgets\Homepage\RecentProductsWidgetView.blade.php

public $controller (string)

If you do not want to put your data method on your widget class, you can set public $controller = App\Some\Class\MyController::class and put your public data method on a dedicated class.(instead od having it on your widget class)

public $presenter (string)

If you do not want to put your present method on your widget class, you can set public $presenter = App\Some\Class\MyPresenter::class and put your public present method on a dedicated class.The data retured from your controller is first piped to your presenter and then to your view.(So if you specify a presenter your view file gets its data from the presenter and not the controller.)

public $cacheLifeTime (int)

If you want to override the global cache life time (which is set in your config file) for a specific widget, you can set $cacheLifeTime on your widget class.

value effect
-1 forever
'forever' forever
0 disable
1 1 minute

public $cacheTags (array)

You can set public $cacheTags = ['tag1','tag2'] to exactly target a group of widgets to flush their cached state. using the helper function :

expire_widgets(['someTag', 'someOtherTag']);

This causes all the widgets with 'someTag' and 'someOtherTag' to be refreshed.

Note: Tagging feature works with ALL the laravel cache drivers including 'file' and 'database'.

public function extraCacheKeyDependency

It is important to note that if your final widget HTML output depends on PHP's super global variables and you want to cache it,Then they must be included in the cache key of the widget. So for example :

public function cacheKey

If you want to explicitly define the cache key used to store the html result of your widget, you can implement this method.

namespace App\Widgets;

class MyWidget
{

    public function data()
    {
        $id = request('order_id');
        return Product::where('order_id', $id)->get();
    }
    

    public function extraCacheKeyDependency()
    {
        return request()->get('order_id');
    }
    
}

You may want to look at the source code and read the comments for more information.

Tip: If you decide to use some other template engine instead of Blade it would be no problem.

🐍 What is our problems? 🐍

Problem 1 : Controllers easily get crowded 😩

Imagine An online shop like amazon which shows the list of products, popular products, etc (in the sidebar), user data and basket data in the navbar and a tree of product categories in the menu and etc... In traditional good old MVC model you have a single controller method to provide all the widgets with data. You can immidiately see that you are violating the SRP (Single Responsibility Priciple)!!! The trouble is worse when the client changes his mind over time and asks the deveploper to add, remove and modify those widgets on the page. And it always happens. Clients do change their minds.The developoer's job is to be ready to cope with that as effortlessly as possible.

Problem 2 : Page caching is always hard (But no more) 😞

Trying to cache the pages which include user specific data (for example the username on the top menu) is a often fruitless. Because each user sees slightly different page from other users. Or in cases when we have some parts of the page which update frequently and some other parts which change rarly... we have to expire the entire page cache to match the most frequently updated one. :( AAAAAAAAAhh...

Problem 3 : View templates easily get littered with if/else blocks 😵

We ideally want our view files to be as logic-less as possible and very much like the final output HTML.Don't we ?! if/else blocks and other computations are always irritating within our views. specially for static page designers in our team. We just want to print out already defined variables wiout the to decide what to print. Anyway the data we store in database are sometimes far from ready to be printed on the page.

🎯 What is the solution?

So, How to fight against those ?

The main idea is simple, Instead of one controller method to handle all widgets of the page, Each widget should have it's own controller class, view partial, view presenter class and cache config, isolated from others. That's it !! :) This idea originally comes from the client-side js frameworks and is somewhat new in server-side world.

📖 Design Patterns Theory

The widget object pattern is in fact a variation of the famous single responsibility principle. Instead of having one bloated controller method that was resposible to supply data for all the widgets... You distribute your controller code amougst multiple widget classes.(Each widget is responsible for small portion of the page.)

It helps you to conforms to Open-closed principle.Because if you want to add a widget on your page you do not need to add to the controller code. Instead you create a new widget class from scratch or when you want to remove something from the page you do not have go to the controller find and comment out related controller code. removing the @widget('myWidget') is enough to disable the corresponding controller and hence db queries.

How to referrence widget controllers from routes ?

This way you can also expose your data as json for client-side apps.

Route::get('/api/recent-products', '\App\Widgets\MyWidget@data');

* It is important to put \ before App when you wnat to refer to a class outside the Http\Controller folder.

How to expose a widget content directly from a url ?

Route::widget('/some-url', 'MyWidget', 'MyRouteName1');
// or
Route::jsonWidget('/my-api','MyWidget', 'MyRouteName2');

A GET request to /some-url/{a}/{b} will see the widget. a and b parameters are passed to widget controller.

jsonWidget will expose the cached data returned from the widget's controller.

You can also :

Route::view('/some-url-2', 'someView', 'theRouteName'); //loads resource/views/someView.blade.php with a GET

🙋 Contributing

If you find an issue, or have a better way to do something, feel free to open an issue or a pull request.

❗️ Security

If you discover any security related issues, please email imanghafoori1@gmail.com instead of using the issue tracker.

⭐️ Your Stars Make Us Do More ⭐️

As always if you found this package useful and you want to encourage us to maintain and work on it. Just press the star button to declare your willing.