August 31, 2015 11:10

Router binding in Laravel used in conjunction with middleware makes model management really easy. No more passing in IDs, loading models, and checking permissions for every controller method.

This example makes use of my ślimak slugged models package

class Album extends SluggedModel
{
    protected $appends = ['url'];

    /**
     * @return string
     */
    public function getUrlAttribute()
    {
        return '/albums/' . $this->slug;
    }
}

In app/Http/routes.php

Route::get('albums/{album}', 'AlbumController@show');
Route::get('albums/{album}/edit', 'AlbumController@edit');

In app/Providers/RouteServiceProvider.php

$router->bind('album', function ($slug) {
    return \App\Album::findBySlug($slug);
});

Finally, in your controller:

use App\Album;

class AlbumController extends Controller
{
    public function show(Album $album)
    {
        return view('albums.show')
            ->with('album', $album);
    }
}

Now when linking to your album's permalink in Blade, you can simply link to {{ $album->url }}. To link to the edit URL, just link to {{ $album->url }}/edit

This gets even more useful when you start to add middleware. For example, I can check to make sure only the album's owner, and admins, have permission to edit the album.

class Owner {
    public function handle($request, Closure $next, $resource)
    {
        $user = \Auth::user();

        if ($request->{$resource} && $request->{$resource}->user_id != $user->id && ! $user->is_admin) {
            abort(403);
        }

        return $next($request);
    }
}

Then if I add this middleware to the controller it takes care of that protection easily:

class AlbumController extends Controller
{
    public function edit(Album $album)
    {
        $this->middleware('owner:album');

        return view('albums.edit')
            ->with('album', $album);
    }
}

Laravel, PHP

August 28, 2015 23:33

I finally finished converting my site entirely into Laravel 5.

In addition, I've started using a bunch of awesome packages:

  • Parsedown (all blog posts are written in Markdown now)
  • SimpleImage to resize photos
  • Highlight.js for syntax highlighting in blog posts (see the previous post)
  • Dropzone for drag and drop photo uploads (in my private admin section)
  • Font Awesome for scalable icons in a bunch of places
  • Bower to manage all of those front-end dependencies and keep my repository clean

I've also done some major redesign, the most noticeable ones being the homepage and photography pages. Photos now scale to fit your window. As a result I decided to increase the quality of the photos as well, from 800 pixels wide to 1600.

News, Website

August 25, 2015 15:45

Here is a simple way to manage storing and displaying dates and times in Laravel in the user's timezone.

First of all, we are going to store all dates and times in UTC. In Laravel, make sure that the timezone is set to UTC in config/app.php

Second, you'll need a way to let users specify their timezone. I'll leave that up to the reader for now. You can either do this through a settings page, or auto-detect it with JavaScript. I prefer to store timezones as an hourly offset from UTC, e.g. -5 for Central time, -4 for Eastern.

Finally, we'll have a base model that all of our Eloquent models extend. If you already have one you can just add this function to it. Save this into app/Model.php

namespace App;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model as Eloquent;

class Model extends Eloquent
{
    /**
     * Display timestamps in user's timezone
     */
    protected function asDateTime($value)
    {
        if ($value instanceof Carbon) {
            return $value;
        }

        $me = \Auth::user();
        $tz = $me->timezone ?: \Cookie::get('tz');

        $value = parent::asDateTime($value);

        return $value->addHours($tz);
    }
}

Make sure to update all of your existing models to extend the new model class instead of the Eloquent model.

Eloquent has an asDateTime function which converts MySQL timestamps to Carbon entities. It calls this function both when setting and getting timestamps. So first we check if the $value is already a Carbon instance - if so, we return it without any adjustments, because when setting a timestamp we do NOT want to apply the timezone adjustment. **

The next two lines are simply retrieving the user's timezone from one of two sources, an attribute or a cookie. If you are setting the cookie through JavaScript, make sure it is added as an exception to the EncryptCookies middleware. Again, this value in my case is an hour offset e.g. -5. Finally we get the Carbon instance that Eloquent returns and add the hour offset.

Now all dates and times will be stored in UTC but displayed in the user's timezone!

Two caveats that I've discovered so far. First, when setting timestamps, to make sure to set them as Carbon instances. (You don't need to worry about the created_at, updated_at, or deleted_at timestamps, just any others that you use). For example, do this:

$post->scheduled_at = Carbon::now();

Not these:

$post->scheduled_at = \DB::raw('now');
$post->scheduled_at = date('Y-m-d H:i:s');

Otherwise your timestamps will not be stored in UTC.

The more annoying caveat is that for certain Carbon methods, you need to undo the hour adjustment in order for them to work correctly. The two that I've discovered so far are age and diffForHumans, because both of those methods compare the given timestamp to the current timestamp (in UTC). So if your user's timezone is -5 hours, and you ask for $date->diffForHumans() on a brand new timestamp, it'll falsely report that the date is 5 hours ago. I personally added a helper method undo_tz to wrap around those methods. For example:

echo undo_tz($post->created_at)->diffForHumans();

And the method:

function undo_tz(Carbon $date)
{
    $me = \Auth::user();
    $tz = $me->timezone ?: \Cookie::get('tz');
    return $date->subHours($tz);
}

Laravel, PHP

August 22, 2015 21:28

I've recently discovered how great collections are in Laravel.

No more array_summing or looping. Collections are easy to extend. First add this method to your model:

public function newCollection(array $models = [])
{
    return new TransactionCollection($models);
}

Then create the TransactionCollection class and extend it as you wish. For example:

class TransactionCollection extends Illuminate\Database\Eloquent\Collection
{
    public function total()
    {
        return $this->sum('amount');
    }
}

Now you can easily display the total of all transactions with $transactions->total()

Laravel, PHP

August 9, 2014 06:08

If you know me, you know that I'm a little OCD. It was killing me that my gmail and iPhone contacts were out of sync. So here's what I did to fix that:

  1. First turn on "Contacts" for iCloud in my phone settings
  2. Now all of my phone contacts are on iCloud
  3. Log in to icloud.com, and export all contacts as vCard file
  4. Go to Gmail and import the vCard file
  5. This creates a new group, "Imported on [Date]" with all of the phone contacts
  6. Find and merge duplicates
  7. Go through other groups and remove contacts from the "Imported on [Date]" group
  8. Now the only contacts left in the "Imported on [Date]" group are the new ones that were on the phone and not in gmail
  9. Organize and clean up those contacts, and delete the "Imported on [Date]" group
  10. Now Gmail has ALL of my contacts, organized in the way I want
  11. Go to the iPhone settings > Mail, Contacts, Calendars. Add a new Google account and enable only "Contacts" for this account
  12. Go to Mail, Contacts, Calendars, and select this Google account as the Default account for Contacts.
  13. Disable "Contacts" for iCloud.
  14. Now any new contacts you create on your phone will be automatically copied to Gmail
  15. The last step is to ensure that any new contacts created in Gmail will also be automatically copied to your phone. In Mail, Contacts, Calendar > Fetch New Data, scroll to the bottom and select "Every 15 Minutes" for Fetch.
  16. Don't forget to backup your phone so that you don't have to go back and redo all of this.

All done! Now my existing contacts are synced on both accounts, and any create, update, or delete actions are synced both ways.

Other