Mike Classic

Laravel Archives

Collection Class: Array Mapping

May/07/2014

Thank you, Laravel, for your Illuminate package. Thank you for the Collection class.

Recently I had a situation where I had to query a database, return some results, and convert the entries from objects into strings. In vanilla PHP, one can do this with something as simple as array_map.

I figured, since I was working with the Laravel framework, that I'd use the Illuminate\Support\Collection class, as I knew it was quite easy to use, handy, and a great enhancement to PHP's array functions.

While I was digging through the Collection class source, I noticed different array mapping functions, so I had to determine which was best to use for my situation.

Each()

This method simply executed the array_map function with no frills. It didn't save it either. So basically the user uses it to perform external actions, such as building a new array or object with it, used outside of the existing Collection instance.

    /* Collection method: */
    public function each(Closure $callback) {
        array_map($callback, $this->items);
    }

    /* Sample usage. */
    $percentages = array();
    $collection->each(function($item) use (&$percentages)
    {
        $percentages[] = $item * 100;
    });

Map()

The map method also uses array_map, but passes you the array keys as well, in case you need to use them. Additionally, it returns a new Collection instance. This could be useful if, say, you want to further manipulate your new array but want to preserve the existing instance as well.

    /* Collection method: */
    public function map(Closure $callback)
    {
        return new static(array_map($callback, $this->items, array_keys($this->items)));
    }

    /* Sample usage. */
    $newCollection = $collection->map(function($item, $key)
    {
        return array($key => $item + 1);
    });

Transform()

And finally, we come to transform. This one was my saviour tonight. It allowed me to manipulate the Collection in place. That is, it also uses array_map but manipulates the array in-place. This allowed me, in my example, to convert an array of objects into an array of strings without having to waste memory by creating a second array.

    /* Collection method: */
    public function transform(Closure $callback)
    {
        $this->items = array_map($callback, $this->items);

        return $this;
    }

    /* Sample usage. */
    /* Registrations before:
     * $registrations[0] = new \stdClass() { public $registration_id = '1234' };
     * Registrations after:
     * $registrations[0] = '1234';
     */
    $registrations->transform(function($item)
    {
        return (string) $item->registration_id;
    });

Laravel: Mass Inserts Using DB Query Builder

Apr/20/2014

When passing arrays for mass insert into the DB QueryBuilder, make sure each array entry has the same fields to populate, no extras. This may be intuitive for you, but it was not for me.

Symptoms

The problem manifested itself during database seeding. Here is a sample schema for the type of table I was building and seeding.

Schema::create('services', function(Blueprint $table)
{
    $table->primary('id');
    $table->boolean('auxiliary')->default(false);
    $table->decimal('rate');
});

I had a seeder class which had an array of entries to insert. This is basically an array of arrays, each nested array represents one table row. This is demonstrated below:

class ServicesTableSeeder extends Seeder {
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        DB::table('services')->delete();

        $entries = [
            [
                'rate' => 50.00,
                'description' => 'Cheaper Service'
            ],
            [
                'rate' => 100.00,
                'description' => 'Some Service',
                'auxiliary' => true
            ],
            /* ... */
        ];

        DB::table('services')->insert($entries);
    }
}

As you can see, one row had more fields to populate than the previous. I had thought that the query builder would accomodate for this, but it doesn't. I'd run this seeder, and the table would simply not populate at all, with no errors thrown.

The solution I came up with was to run array inserts where the rows/entries all populate the same fields. So if you have rows that populate more/less/different fields, group them together in a separate array of entries, as such:

class ServicesTableSeeder extends Seeder {
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        DB::table('services')->delete();

        $entries = [
            [
                'rate' => 50.00,
                'description' => 'Cheaper Service'
            ],
            [
                'rate' => 60.00,
                'description' => 'Medium Service'
            ]
        ];

        $entries2 = [
            [
                'rate' => 100.00,
                'description' => 'Some auxiliary service',
                'auxiliary' => true
            ],
            [
                'rate' => 90.00,
                'description' => 'Explicitly non-auxiliary service',
                'auxiliary' => false
            ]
        ];

        DB::table('services')->insert($entries);
        DB::table('services')->insert($entries2);
    }
}

Notice how the two arrays of entries have the same fields to populate.

Laravel: App Environment Detection

Mar/20/2014

When I develop Laravel apps, I do so in several different environments. As per the Laravel Docs, by default, the environment in which your app runs is defined in bootstrap/start.php as an array which maps hostnames of machines to whichever environments you want them to run.

You can make as many or as few environments as you want. It's recommended to create three separate environments, but again, there's no rule set in stone. These environments are: development, staging, & production. I used to have that set up as such:

$env = $app->detectEnvironment(array(
    'development' => array('vagrant-machine'),
    'staging' => array('staging.somehost.xyz'),
    'production' => array('www.yourapp.xyz')
));

One can also have multiple machine names mapped to an environment, so if you wanted to have two machines set up in your development environment, you could implement like so:

$env = $app->detectEnvironment(array(
    'development' => array('vagrant-machine', 'another-machine'),
    'staging' => array('staging.somehost.xyz'),
    'production' => array('www.yourapp.xyz')
));

It's a beautiful thing, being this simple and configurable.

Some people like to use environment variables so that they don't have to keep remembering a bunch of hostnames, especially if they deploy to multiple servers or instances. This can be accomplished by providing a Closure instead of an array to Laravel's $app->detectEnvironment() method:

$env = $app->detectEnvironment(function()
{
    return getenv('LARAVEL_ENV');
});

At this point, you could set your environment variable through Apache's .htaccess file:

SetEnv LARAVEL_ENV "development"

It's as simple as that.

Call me crazy, or stupid, but I actually have a mixture of both. If the environment variable has not been set, then fall back to the array of hostnames to determine environment.

$env = $app->detectEnvironment(function()
{
    // Set up the environments fall-back array
    $environments = array(
        'vagrant' => array('vagrant-box'),
        'local' => array('my-laptop')
    );

    // Attempt to determine via environment variable first
    $env = getenv('LARAVEL_ENV');
    if (!empty($env)) {
        return $env;
    } else {
        /*
         * Environment variable empty or invalid, proceed to sort
         * through fall-back hostname array
         */
        $hostname = gethostname();
        foreach ($environments as $environment => $hosts) {
            foreach ((array) $hosts as $host) {
                if (!strcasecmp($host, $hostname)) {
                    // Found, return mapped hostname, we're outta here
                    return $environment;
                }
            }
        }

        // Nothing matched, in array nor within environment veriable(s)
        return 'production';
    }
});