October 13, 2015 19:36
I actually thought of this while answering a question on StackOverflow
When querying sums of columns, I typically do something like this:
$query = $account->transactions()
->select(\DB::raw('SUM(amount) AS total'))
->first();
$total = $query->total;
Here's another way to accomplish the same thing. Of course if you end up fetching thousands of rows you'll probably want to just let MySQL do the math for you, but it's way easier to read and cleaner.
$amounts = $account->transactions()->lists('amount');
$total = $amounts->sum();
September 17, 2015 14:32
In Laravel, redirecting the user to one URL after logging in and another after registering (such as a welcome page) is very simple.
First, inside your AuthController
, set the post-login URL as the default:
protected $redirectTo = '/url-after-login';
and then inside the create function, override it to the post-registration URL
protected function create(array $data)
{
$this->redirectTo = '/url-after-register';
return User::create([...]);
}
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);
}
}
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);
}
August 22, 2015 21:28
I've recently discovered how great collections are in Laravel.
No more array_sum
ming 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()
- 1
- 2