Yearly Archives: 2018


Using Browser-Sync with Laravel … without Mix or Webpack

Prompted by a question on Laracasts, I wanted to see if I could use Browsersync to automatically refresh the browser after each code change.  I mostly do back-end development, but all the docs about Browsersync are related to front-end JS and CSS development.  My needs were simpler.  I don’t have a front-end build process such as Laravel Mix, and wanted to monitor my view files and controllers etc.

After reading a little about Browsersync, I have my browser reloading automatically any time a view is saved, or anything in the app folder. It was quite easy to setup.

Install Browsersync

in the root of your laravel project, install Browsersync globally;

> npm install -g browser-sync

Create a Browsersync init file

> browser-sync init

Edit the Config file

Open Browsersync init file bs-config.js in your editor and tell it what folders to monitor;

"files": ["resources/views/**","app/**","public/**"],

and where to send the sync messages (within socket:)

"domain": 'localhost:3000'

Launch Browsersync

From the console, start Browsersync

> browser-sync start --config bs-config.js

You should now be able to navigate to the Browsersync UI in the browser at http://localhost:3000

Add javascript client to your application

I know it seems a lot of steps, but these are all really quick. The final step is to add the Browsersync javascript to your project – inside a check to see if we are in development mode.

In a master layout file (eg app.blade.php) add the following before the closing tag.

@if (getenv('APP_ENV') === 'local')
    <script id="__bs_script__">//<![CDATA[
    document.write("<script async src='http://HOST:3000/browser-sync/browser-sync-client.js?v=2.24.4'><\/script>".replace("HOST", location.hostname));
    //]]></script>
@endif

 

Gotchas

The above is working on OSX using Valet+Nginx as the web server – your mileage may vary.

If your code crashes and whoops appears, then obviously the browsersync code is no longer running in the browser, and after you fix the issue you will need to refresh manually.  I wonder if its possible to extend the whoops handler….

I was able to get Whoops handler to include the browsersync javascript but only by editing the file in the vendor folder – which is never a good idea;

vendor/filp/whoops/src/Whoops/Resources/views/layout.html.php

put the browsersync code before the closing body tag

Shout out to JH – I have been to their offices several times for meetings of phpMinds in Nottingham, and not realised Browsersync was their open source project.

 


Laravel Blade to deal with null dates

This example is for Laravel 5.5 and should be compatible with 5.6 onwards but no guarantees!

The problem

When using date and datetime fields in Laravel it is really useful to add them to the $dates array in the Eloquent model so that they are automatically cast to Carbon instances.

Then in the views, the format can be easily specified;

<td>{{ $task->completed_at->format('d-m-Y H:i') }}</td>

This works fine until the date is null. In our example, the task completed date is not set until the task is completed, and with the above code, sometimes we are trying to apply Carbon format to a null object.

The view then starts to get messy, checking if the date is null before outputting the Carbon format.

Enter Blade.  Using Blade we can hide some of this complexity from our views.

If you haven’t written Blade directives before, they can be confusing. The purpose of a blade directive is to return a string of PHP that can be inserted into the cached view file, and then executed later, when the view is rendered.  This means it is not possible to directly interact with any of your data within the blade component itself.

 Creating a blade directive

If you are creating just a single blade Directive then its possible to just put this in the AppServiceProvider, but as I might create more, and also be able to easily lift this and put it in other projects, I decided to create a BladeServiceProvider.php file and then put all my directives in there.

Start with the template for a Service Provider  php artisan make:provider BladeServiceProvider

and add this new provider to your config/app.php file

/*
 * Application Service Providers...
 */
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\BladeServiceProvider::class,

Add our new directive to the BladeServiceProvider  boot method

public function boot()
{
    /**
     * @date blade directive 
     * use as @date($object->datefield) 
     * or with a format @date($object->datefield,'m/d/Y')
     */
    Blade::directive('date', function ($expression) {

        $default = "'d-m-Y H:i'";           //set default format if not present in $expression
        
        $parts = str_getcsv($expression);

        $parts[1] = (isset($parts[1]))?$parts[1]:$default;

        return '<?php if(' . $parts[0] . '){ echo e(' . $parts[0] . '->format(' . $parts[1] . ')); } ?>';
    });
}

Line 8 is the start of our Blade definition.  Make sure you import Illuminate\Support\Facades\Blade  at the top of the service provider. Here we declare ‘date’ is the name of our new directive. When the Blade extension is used it will be passed $expression, and this will be a single string from which we have to parse our parameters.

Line 12 breaks the expression into multiple parts as an array

Line 14 ensures that we have a $parts[1] element so that we can always pass a string to the format method.  The default value is specified on line 10 and will be used as a fallback if the format is not specified in our view.

Finally, Line 16 is the meat of this Blade. This strange concatenation of strings and parts is building up the final output that will go into our cached view file for processing when the final view is rendered. The function of this is to create an in-line php code that checks if the value is not null and if so, echo it with the Carbon format command appended.  If the value is null then nothing is echoed.

What have we created?

We now have a Blade directive that can format dates if they are not null and optionally accepts a date format

<td>@date($task->completed_at)</td>

<td>@date($task->archived_at,'Y-m-d')</td>

This last example, produces Blade output of

<td><?php if($task->archived_at){ echo e($task->archived_at->format('Y-m-d')); } ?></td>

Tips!

Working with Blade can be extremely time consuming.  Don’t underestimate the amount of time it takes fiddling with the output string.

Don’t forget, you cannot interact with the data within the blade directive itself.

Always clear cached views php artisan view:clear each time you make a change to your BladeServiceProvider


Using FontAwsome SVG and Laravel Blade to output a star rating

Simple code snippet to put out star outlines, stars and half stars as a rating

Uses Font Awesome (https://fontawesome.com/) for SVG icons

@foreach(range(1,5) as $i)
    <span class="fa-stack" style="width:1em">
        <i class="far fa-star fa-stack-1x"></i>

        @if($rating >0)
            @if($rating >0.5)
                <i class="fas fa-star fa-stack-1x"></i>
            @else
                <i class="fas fa-star-half fa-stack-1x"></i>
            @endif
        @endif
        @php $rating--; @endphp
    </span>
@endforeach

Note that $rating is expected to contain a value 0 to 5 (including fractions).  It will be affected by the $rating--; so keep a copy if you need it later.

By altering the stack order, the stars can be outlined.

 


Allow user to choose pagination length via dropdown (Laravel)

The following technique allows the user of your site to choose the pagination length from a dropdown menu.

In this example, the members of a club are paginated.

When displaying the links, we need to append the current page length

{{ $members->appends(compact('items'))->links() }}

The Select list is created as follows

<form>
    <select id="pagination">
        <option value="5" @if($items == 5) selected @endif >5</option>
        <option value="10" @if($items == 10) selected @endif >10</option>
        <option value="25" @if($items == 25) selected @endif >25</option>
    </select>
</form>
<script>
    document.getElementById('pagination').onchange = function() { 
        window.location = "{!! $members->url(1) !!}&items=" + this.value; 
    }; 
</script>

Changing the dropdown causes the page to be reloaded, passing `items=10` etc to the controller. The controller checks for items or uses a default value.

    public function index(Request $request)
    {
        $items = $request->items ?? 10;      // get the pagination number or a default

        $club = Club::findOrFail(session('club'));

        $members = $club->members()->paginate($items);
       
        return view('club.' . config('app.locale') . '.index')
            ->withClub($club)
            ->withMembers($members)
            ->withItems($items);
    }

In the controller method, $itemsis passed back to the view so that it can be used to ensure the current length is selected