28
Nov '20

One of the sites I run is a craft business where we want to have a paginated product listing on the front page, but we also want it to be randomised, so that you don’t always get the same products when you first load it.

I wasn’t even sure this was possible when I began looking into it, but thanks to PHP’s ability to seed the randomisation functions, and a bit of careful tapping into Laravel’s core pagination library it is possible.

I’ve split the logic into two functions. One that generates a repeatably randomised set of product records, and another that gives the ability to use Laravel’s paginator on a Collection instance, rather than a query builder instance:

<?php

use Illuminate\Support\Collection;
use Illuminate\Pagination\Paginator;
use Illuminate\Pagination\LengthAwarePaginator;

class ProductController extends \Controller
{
    /**
     * Product index page
     *
     * @return \Illuminate\Contracts\Support\Renderable
     */
    public function index()
    {
        return view('product.index')->with('title', 'Products')
                    ->with('products', $this->randomisedPaginatedProducts());
    }

    /**
     * Get randomised paginated products
     * @return \Collection
     */
    protected function randomisedPaginatedProducts() {

        if(session()->has('seed'))
            $seed = session('seed');
        else {
            $seed = rand(1, 100);
            session()->put('seed', $seed);
        }

        mt_srand($seed);

        $records = \App\Models\Product::get();

        $range = range(0, $records->count() - 1);
        shuffle($range);

        $randomised = collect($range)->map(function($index) use ($records) {
            return $records[$index];
        });

        return $this->paginateCollection($randomised);
    }

    /**
     * Paginate collection
     *
     * @param Collection $items
     * @param int $per_page
     * @param int|null $page
     * @param array $options (path, query, fragment, pageName)
     * @return LengthAwarePaginator
     */
    protected function paginateCollection(Collection $items, int $per_page = null, int $current_page = null, array $options = [])
    {
        $current_page = $current_page ?: (Paginator::resolveCurrentPage() ?: 1);

        return new LengthAwarePaginator($items->forPage($current_page, $per_page), $items->count(), $per_page, $current_page, $options);
    }
}

The key to this is the seed that we generate and keep in the session (we could equally pass it around in the pagination links if we wanted). The seed here is a number between 1 and 100, but it could be anything, really this just means that there are 100 different combinations of products it could come up with. This seed is used to generate a randomised array of numbers which will correspond to the numeric keys of the list of products, and then the products are sorted into a new array accordingly. Pass these to the paginator and you’re done.

Do be aware that this method requires you to pull all the products out of the database upfront, but for a relatively small total product count it’s no issue.

26
Jan '20

When working with Eloquent recordsets, I tend to expect them to behave exactly like core Laravel collections. Indeed why wouldn’t they? Why wouldn’t a recordset exist as a collection, with all the properties and functions that come with them?
On the most part you can use an Eloquent collection just like a core Laravel collection, however some functions have different behaviours, such as the ‘merge’ function, which in Eloquent is hard-wired to key by the record primary key, and will overwrite a duplicate key, even if the records are otherwise different.

Enter the completely undocumented ‘toBase’ function.

This is buried within the Eloquent collection class, and will spit out the recordset as a core Laravel collection, and allow you to perform all the normal collection operations in the manner you would expect.

13
Dec '19

Very quick one, because this took me waaaaaaay too long to figure out, due to the error message having no apparent relation to the issue. If you get presented with the error:

Fatal error: Uncaught RuntimeException: A facade root has not been set.

Then this probably means that there is a syntax error in one of your config files.
I hope that’ll save a few hours of head-scratching!

05
Oct '19

We’ve all been there. Customer after customer calls saying they haven’t received the email notifications they were expecting from a system. The issues are pretty much always an incorrect address or an over-enthusiastic spam filter, but even so, I decided to start logging all the notifications sent from my applications. If I can track dates, times, senders, and recipients then I’ve got all the evidence I need if there is a query.

Starting with a table for the logs, allowing for tracking of the notifiable record, the recipient user and the sender:

Schema::create('log_notifications', function ($table) {
    $table->increments('id')->unsigned();
    $table->integer('parent_id');
    $table->string('parent_type');
    $table->integer('user_id')->unsigned()->index();
    $table->integer('sender_id')->unsigned()->index();
    $table->string('email');
    $table->string('type');
    $table->timestamps();

    $table->index(['parent_id', 'parent_type']);
});

Then the main code is a trait that can be used by all notifiable models:

trait Notifiable {

    use \Illuminate\Notifications\Notifiable {
        notify as coreNotify;
    }

    /**
     * Log notification relationship
     */
    public function logNotifications()
    {
        return $this->morphMany('\App\Model\LogNotification', 'parent');
    }

    /**
     * Get Sender ID attribute
     * @param  mixed  $value Original value
     * @return integer
     */
    public function getNotifiableSenderIdAttribute($value) {
        if($value)
            return $value;

        return \Auth::check() ? \Auth::user()->id : 0;
    }

    /**
     * Get User ID attribute
     * @param  mixed  $value Original value
     * @return integer
     */
    public function getNotifiableUserIdAttribute($value) {
        if($value)
            return $value;

        if(in_array(class_basename(get_called_class()), ['User']))
            return $this->id;

        return \Auth::check() ? \Auth::user()->id : 0;
    }

    /**
     * Send the given notification
     * @param  mixed  $notification
     * @return void
     */
    public function notify($instance)
    {
        $this->logNotifications()->create([
            'user_id' => $this->notifiable_user_id,
            'sender_id' => $this->notifiable_sender_id,
            'email' => $this->email,
            'type' => get_class($notification)
        ]);

        $this->coreNotify($notification);
    }
}

This assumes that you have made a model for the notification logs at ‘\App\Model\LogNotification’, but you can change that to however your architecture is set up.

It also assumes a ‘User’ model class that contains your application users. It may be that it’s only your User model that will be notifiable, in which case you can happily drop the polymorphic parent record as it should always match your user_id.

And there you go. not the most eath-shattering of scripts, but definitely a good one to help ensure your notifications are behaving themselves.

13
Sep '19

I recently decided that (after far too long) I needed to get up-to-date with how I manage my package dependencies for one of my bigger projects. I’d been using gulp with bower components hooked into Laravel Elixir basically since I first ported the project to Laravel, and the deprecation was getting beyond a joke.

Despite there being tons of information out there on how to do achieve individual parts of the configuration, nowhere could I find a single example that would show me how to configure NPM for compiling assets to work with Laravel Mix (the version of elixir) and give me a ‘watch’ option to auto-compile new asset changes.

The first job was to rename my gulpfile.js to webpack.mix.js, and then convert the syntax to use Laravel Mix, rather than Elixir. I’m not going to go into the details of that as it’s extremely straightforward.

Removing bower was simply a case of finding the relevant packages in npm, and amending the paths in the webpack.mix.js accordingly.

The hardest part was getting NPM’s package.json setup with everything I needed. All references to gulp and laravel-elixir came out, webpack and webpack-cli went in along with npm-watch and laravel-mix. My full package.json is below:

{
  "private": true,
  "devDependencies": {
    "resolve-url-loader": "^3.1.0",
    "sass-loader": "^7.3.1",
    "webpack": "^4.39.3",
    "webpack-cli": "^3.3.8"
  },
  "dependencies": {
    "@fortawesome/fontawesome-free": "^5.10.2",
    "@storyous/responsive-bootstrap-toolkit": "^2.6.1",
    "babel-core": "^6.26.3",
    "bootstrap": "^3.4.1",
    "bootstrap-datepicker": "^1.6.4",
    "bootstrap-sass": "^3.3.7",
    "clipboard": "^2.0.1",
    "datatables": "^1.10.18",
    "datatables-responsive": "^1.0.7",
    "datatables.net": "^1.10.19",
    "datatables.net-bs": "^1.10.19",
    "datatables.net-plugins": "^1.10.19",
    "graceful-fs": "^4.0.0",
    "jquery": "^3.4.1",
    "jquery-ui": "^1.12.1",
    "jquery-ui-dist": "^1.12.1",
    "jquery-validation": "^1.19.1",
    "laravel-mix": "^4.1.4",
    "node-sass": "^4.12.0",
    "npm-watch": "^0.6.0",
    "pnotify": "^4.0.0",
    "requirejs": "^2.3.6",
    "select2": "^4.0.10"
  },
  "watch": {
    "dev": {
      "patterns": [
        "resources/assets"
      ],
      "extensions": "js,css",
      "quiet": false,
      "delay": 1000,
      "runOnChangeOnly": true
    }
  },
  "scripts": {
    "dev": "node_modules/.bin/webpack --config=node_modules/laravel-mix/setup/webpack.config.js",
    "start": "npm run dev",
    "watch": "node_modules/.bin/npm-watch"
  }
}

With packages in place the last part was to set up the script definitions to compile my assets. The definitions shown will allow the following commands:

npm run dev – Compiles all assets following the script in webpack.min.js

npm run watch dev – Launches an active task that watches all CSS and JS files in resources/assets for changes, and recompiles whenever changes are detected.

So that’s a working overview start to finish. Hopefully it helps to see the setup in its entirety. It’s a really slick system once it’s all configured, but good grief it takes some effort to get there!

25
Jul '18

I know I know. Rely too much on traits and you’re asking for trouble. But when used correctly, for self-contained bolt-on functionality they really can be fantastic and save loads of time.

There only condition for this logic is that any record with which you are using this must have a ‘logo’ field. Yeah I know I could have done a more advanced version with a separate logo table and a polymorphic relationship, but it’s not like a standard system will have 2 dozen records that all require logos against them so I’ve kept it simple.

I’ve used the word ‘logo’ here, but it’s a generic image storing trait and could be used for anything. You’ll also notice an optional config option of ‘app.directory.logo’, which, if provided, will be used as a relative path below the application’s public folder, otherwise ‘/logos’ will be used.

This assumes that you have an image referenced as ‘logo_file’ in your request and have triggered a save on the parent record.

namespace App\Trt;

trait Logo
{
    /**
     * Custom boot method
     */
    public static function bootLogo()
    {
        /*
         * Trigger on save
         */
        static::saved(function ($record) {

            try {
                if(request()->hasFile('logo_file') && request()->file('logo_file')->isValid()) {
                    $logo_file = request()->file('logo_file');
                    $logo_folder = public_path(config('app.directory.logo', '/logos'));

                    if(is_dir($logo_folder) && is_writable($logo_folder)) {
                        //Prepending the record ID to ensure uniqueness
                        //Could also prepend parent record type / sanitised model name if it's to be used for multiple records
                        $file_name = $record->id . '_' . $logo_file->getClientOriginalName();

                        $record->clearLogo(false);

                        if($logo_file->move($logo_folder, $file_name)) {
                            $record->logo = $file_name;

                            //Direct update here so we don't trigger any callbacks
                            \DB::table($record->getTable())->where('id', $record->id)->update(['logo' => $file_name]);
                        }
                    }
                    else {
                        \Log::error('Logo folder is not writable ' . $logo_folder);
                    }
                }
                elseif(request()->clear_logo) {
                    $record->clearLogo();
                }
            }
            catch(\Exception $e) {

            }
        });

        /*
         * Trigger on delete
         */
        static::deleting(function ($record) {
            $record->clearLogo(false);
        });
    }

    /**
     * Check for logo
     * @return boolean
     */
    public function hasLogo() {
        return ($this->logo && is_file($this->logoPath()));
    }

    /**
     * Logo URL
     * @return string|false
     */
    public function logoURL() {
        return $this->logo ? url(config('app.directory.logo', '/logos') . '/' . $this->logo) : false;
    }

    /**
     * Logo Path
     * @return string|false
     */
    public function logoPath() {
        return $this->logo ? public_path(config('app.directory.logo', '/logos') . '/' . $this->logo) : false;
    }

    /**
     * Clear Logo
     * @param boolean [$save=true] Save
     * @return boolean
     */
    public function clearLogo($save = true) {
        if($this->logo) {
            if($this->hasLogo())
                unlink($this->logoPath());

            $this->logo = null;

            if($save)
                $this->save();

            return true;
        }

        return false;
    }
}

And that’s it!

01
Jun '18

I’m sure we’ve all been there. You create a nice data structure for a utility and the client says “Ok, but I also need to be able to add a note to it”. You add a note field and then they say “Ok, but I also need to add a note over here”. You add another note field over there and then they say “But I need to be able to add new notes and keep the old ones”. You scream internally, wish they’d told you that in the first place, and start building a more flexible solution.

Well here is my take on it. It’s a separate notes table with a polymorphic association to any and all other records. Their models simply need to use this ‘Notable’ trait. The notes will be created automatically with any save action on the parent model, and have a simple functions to retrieve all notes against a parent, or clear them. Simples.

This assumes that you have either a single ‘note’ parameter in your request, or a ‘notes’ array, and have triggered a save on the parent record.

Table structure:

CREATE TABLE `notes` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `parent_id` int(11) NOT NULL,
  `parent_type` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `content` text COLLATE utf8_unicode_ci NOT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
);

Trait:

namespace App\Trt;

/**
 * Notable trait
 */
trait Notable
{
    /**
     * Custom boot method
     */
    public static function bootNotable()
    {
        /*
         * Trigger on create
         */
        static::saved(function ($record) {
            //We check the class name here as otherwise it will get in an infinite loop of trying to add notes below notes
            if(class_basename(get_class($record)) != 'Note' && (request()->note || request()->notes)) {

                if(request()->note) {
                    $record->notes()->create(['content' => request()->note]);
                }

                if(request()->notes && is_array(request()->notes)) {
                    foreach(request()->notes as $note) {
                        $record->notes()->create(['content' => $note]);
                    }
                }
            }
        });

        /*
         * Trigger on delete
         */
        static::deleting(function ($record) {
            $record->clearNotes();
        });
    }

    /**
     * Note relationship
     */
    public function notes()
    {
        return $this->morphMany('\App\Model\Note', 'parent');
    }

    /**
     * Clear all notes
     */
    public function clearNotes() {
        $this->notes()->delete();
    }
}

I tend to include the ID of the current user in the note too, so that I can display the user name against the note text. Simple case of adding the column to the table and the fillable array, adding the relationship function, and replacing:

$record->notes()->create(['content' => request()->note]);

with:

$record->notes()->create(['content' => request()->note, 'user_id' => \Auth::user()->id]);
15
Apr '18

One of my projects features a survey / audit system for which I was tasked to provide an offline facility by allowing the download of fillable PDF forms, and then the subsequent re-upload and processing of them.

For the form generation and download I built a wrapper around TCPDF, which I may blog about one day, but it was an extremely involved process and today is not that day!

The import and processing routine I implemented using TCPDF (specifically this PDFTK library aliased as ‘PDFTK’ in my app configuration), and ended up being nicely self-contained:

/*
 * Read fields from PDF file
 * @param string $path Path to PDF
 * @return array
 */
public static function read($path) {
    $pdf = new \PDFRead($path, ['_command' => '/usr/bin/pdftk']);

    $ignore_fields = ['Validate', 'Submit', 'Reset', 'Print'];
    $ignore_values = ['Select...', 'Off'];

    if($data = $pdf->getDataFields()) {
        $values = collect($data->__toArray())->mapWithKeys(function($data_field) use ($ignore_fields, $ignore_values) {
            if(in_array($data_field['FieldName'], $ignore_fields))
                return [];

            if(isset($data_field['FieldValue'])) {
                $value = $data_field['FieldValue'];

                if($data_field['FieldType'] != 'Text' && in_array($data_field['FieldValue'], $ignore_values))
                    $value = '';
            }
            else
                $value = '';

            return [$data_field['FieldName'] => $value];
        });

        $parsed_values = [];

        foreach($values as $key => $value) {
            if(preg_match('/(.*)\[([0-9\_]+)\]/', $key, $matches)) {
                
                if(!isset($parsed_values[$matches[1]]))
                    $parsed_values[$matches[1]] = [];

                $parsed_values[$matches[1]][$matches[2]] = $value;
            }
            else
                $parsed_values[$key] = $value;
        }

        return $parsed_values;
    }
}

Pass any filled PDF form through that function and it will return an array in the same format as you would get from any normal form submission.
12
Mar '18

Bit of a follow-on here from my post on record versioning. Versioning a record is all well and good, but often you would like to version the reccord’s relations too in order to make the record meaningful, provide context, provide metadata etc.

In the code below, the archive function on the parent model is triggered to create the archive (versioned) record. Note that I have added the with(‘notes’) call, which will add all related records from the ‘notes’ relation into the resulting array. As such this relation has been serialiasied with the parent record.

When the archive (versioned) record is deserialised we will then find that the ‘notes’ are attached to the copy of the parent model, but as simple arrays rather than objects. The custom ‘hydrateArchive’ function simply takes these arrays and turns them back into real models.

Parent Model

/**
 * Archive
 * @return \App\Model\Archive
 */
public function archive() {
    return $this->archives()->create([
        'data' => serialize($this->with('notes')->find($this->id)->toArray())
    ]);
}

/**
 * Custom archive hydrate
 *
 * @return \App\Model\Base
 */
public function hydrateArchive() {
    if(!$this->notes)
        return $this;

    $this->notes = collect($this->notes)->map(function ($data) {
        return new \App\Model\Note($data);
    });

    return $this;
}

Archive Record

/**
 * Deserialise
 * @return \App\Model\Base
 */
public function deserialise() {
    $deserialised = with(new $this->parent_type)->newFromBuilder(unserialize($this->data));
    return $deserialised->hydrateArchive();
}
05
Feb '18

So we all know about Laravel’s built-in ‘creating’, ‘created’, ‘updating’, ‘updated’ etc model events. Seriously useful events for all manner of purposes.

Recently I found myself needing to add a couple more; one to trigger when a model was loaded (for logging purposes), and one before a model was updated (so that I could archive a copy of it before the data was changed).

I came up with the following code and inserted it into an abstract base model class, which all the other models extend.

protected $observables = ['loaded', 'beforeUpdate'];

/**
 * Register a loaded model event with the dispatcher.
 *
 * @param  \Closure|string  $callback
 * @return void
 */
public static function loaded($callback)
{
    static::registerModelEvent('loaded', $callback);
}

/**
 * Register a before update model event with the dispatcher.
 *
 * @param  \Closure|string  $callback
 * @return void
 */
public static function beforeUpdate($callback)
{
    static::registerModelEvent('beforeUpdate', $callback);
}

/**
 * Create a new model instance.
 *
 * @param  array  $attributes
 * @return \Illuminate\Database\Eloquent\Model|static
 */
public function newFromBuilder($attributes = array(), $connection = null)
{
    $instance = parent::newFromBuilder($attributes);

    $instance->fireModelEvent('loaded', false);

    return $instance;
}

/**
 * Trigger before update
 *
 * @param  array  $attributes
 * @return \Illuminate\Database\Eloquent\Model|static
 */
public function update(array $attributes = [], array $options = [])
{
    $this->fireModelEvent('beforeUpdate', false);

    return parent::update($attributes);
}