Skip to content

Instantly share code, notes, and snippets.

@deefour
Last active November 11, 2016 19:22
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save deefour/c6cfcebe808216a874f5 to your computer and use it in GitHub Desktop.
Save deefour/c6cfcebe808216a874f5 to your computer and use it in GitHub Desktop.
A work-in-progress attempt to explain how a few small packages aide me in application development.

NOTE: Some recent, major refactoring has made some sections of this document inaccurate. It will be updated soon.

Handling Requests in Laravel With the Help of Ruby-Inspired Packages

I am a PHP developer that has done a lot of Ruby on Rails development over the last few years. There are a few gems I found especially useful again and again.

I decided to author and publish a series of small, tested PHP packages that attempt to borrow bits of functionality from these gems.

Note: If a method or class appears in an example below without sufficient mention or explanation, refer to the README.md in the above URLs. Full documentation for the packages is provided on their respective repositories.

This document describes how I use the last three packages above to handle the common scenario of editing an existing model, a Podcast, within a Laravel 5.x application.

An Overview of the Deefour\ Packages

A very high-level overview of the role each package plays when handling a request.

  • Authorization for a request is performed by Deefour\Authorizer by way of a "Policy" for the resource being manipulated
  • An "Interactor" holds the business logic to manipulate the resource
  • The attributes of the raw input data found on the request are converted into an expected, consistent format through a "Transformer"
  • The interactor expects a specific "Context" to be passed to it; a class containing all the information it needs to perform it's unit of work.
  • A context object is commonly composed of at least one transformer.

The Setup - Part 1

Note: Some code or best practices are omitted or bypassed in the code blocks below in favor of brevity and clarity.

The Models

The Podcast Model

<?php namespace App;

use Deefour\Authorizer\Contracts\Authorizable as AuthorizableContract;
use Deefour\Authorizer\ResolvesPoliciesAndScopes;
use Illuminate\Database\Eloquent\Model;

class Podcast extends Model implements AuthorizableContract {

  use ResolvesPoliciesAndScopes;
  
  /**
   * {@inheritdoc}
   */
  public function policyNamespace() {
    return 'App\Policies';
  }

}

The User Model

<?php namespace App;

use Deefour\Authorizer\Contracts\Authorizee as AuthorizeeContract;

class User extends Model implements AuthorizeeContract {

}

In order for the application to check if a user is authorized to perform an action on the resource, the model representing the user must be marked as the "Authorizee" and the resource being authorized must be marked as "Authorizable". The policyNamespace() method override tells the application where to look when attempting to automatically resolve a "Policy" class based on a model's FQN for authorization.

Note: In a real application, much of the use of the Deefour\... namespaced traits and related configuration would be pushed up into a generic parent model class to make policy class resolution available and marking all models as authorizable.

The Policy

The PodcastPolicy Policy

<?php namespace App\Policies;

use Deefour\Authorizer\Policy;

class PodcastPolicy extends Policy {

  /**
   * Only the owner of the podcast can modify the record.
   * 
   * @return boolean
   */
  public function update() {
    return $this->record->user_id === $this->user->id;
  }

}

Standalone Authorization

With the models and policy class shown above, authorization can already be performed.

<?php

use Deefour\Authorizer\Authorizer;
use App\User;
use App\Podcast;

$user    = User::find(12);
$podcast = Podcast::find(34);
$policy  = Authorizer::policy($user, $podcast);

$policy->update(); //=> boolean

The Request

The owner of the podcast is logged in and changed a few things on it's edit form. The form was just submitted, sending a PUT request that will be handled by the PostController@update method in a Laravel 5.x application. The request data looks as follows:

id=34
title=Daniel's Digest - Episode 192
tags=life, productivity new-car, personal-finance

If we request all of the above data from an Illuminte\Http\Request instance within a controller action, the following array would be returned

[ 
  'id'    => '34',
  'title' => 'Daniel's Digest - Episode 192',
  'tags'  => 'life, productivity new-car, personal-finance'
]

We should be able to expect data in the following format though.

[ 
  'id'    => 34,
  'title' => 'Daniel's Digest - Episode 192',
  'tags'  => [ 'life', 'productivity', 'new-car', 'personal-finance' ]
]

Let's see how that is done.

The Setup - Part 2

Transformers

The PodcastTransformer Transformer

<?php namespace App\Transformers;

use Deefour\Transformer\Transformer;

class PodcastTransformer extends Transformer {
  
  /**
   * {@inheritdoc}
   */
  $casts = [
    'id' => 'int',
  ];

  protected function title() {
    return ucwords($this->raw('title'));
  }

  protected function tags() {
    return preg_split('/[,\s]+/', strtolower($this->raw('tags')));
  }
  
}

Standalone Transform

With this transformer, the raw input data can be converted into the format our application expects.

use App\Transformers\PodcastTransformer;
use Request; // Illuminate\Http\Request instance via Facade

$transformer = new PodcastTransformer(Request::all());

$transformer->tags; //=> [ 'life', 'productivity', 'new-car', 'personal-finance' ]

Context

The interactor described in the next section requires access to specific data and services to perform it's work. It will expect these dependencies to be available within the context it is given.

The UpdatePodcastContext Context

<?php namespace App\Contexts;

use App\Podcast;
use App\Repositories\PodcastRepository;
use Deefour\Interactor\Context;

class UpdatePodcastContext extends Context {
  
  /**
   * The podcast being updated.
   *
   * @var Podcast
   */
  protected $podcast;  

  public function __construct(Podcast $podcast, PodcastTransformer $transformer) {
    $this->podcast = $podcast;
    
    parent::__construct($transformer);
  }
  
}

The abstract Deefour\Interactor\Context class expects an array of attributes or a transformer to be passed to it's constructor. It provides access to properties and methods on the transformer directly through the context via magic __get() and __call() implementations. This means the 'tags' property could be accessed as shown below.

use App\Podcast;
use App\Contexts\UpdatePodcastContext;
use App\Transformers\PodcastTransformer;
use Request; // Illuminate\Http\Request instance via Facade

$podcast     = Podcast::find(34);
$transformer = new PodcastTransformer(Request::all());

$context = new UpdatePodcastContext($podcast, $transformer);

$context->tags; //=> [ 'life', 'productivity', 'new-car', 'personal-finance' ]

Interactors

The UpdatePodcast Interactor

The final piece before tying everything together in the controller action is the interactor - the object that actually persists the changes to the podcast.

<?php namespace App\Commands;

use App\Contexts\UpdatePodcastContext;
use App\Repositories\PodcastRepository;
use Deefour\Interactor\Interactor;
use Illuminate\Contracts\Bus\SelfHandling;

class UpdatePodcast extends Interactor implements SelfHandling {

  /**
   * The podcast repository.
   *
   * @var Podcasts
   */
  protected $podcasts;

  public function __construct(PodcastRepository $podcasts, UpdatePodcastContext $context) {
    parent::__construct($context);

    $this->podcasts = $podcasts;
  }

  public function handle() {
    $c       = $this->context();
    $podcast = $c->podcast;
    
    $podcast->title = $c->title;

    $this->syncTags($c->tags);

    $podcast->save();
  }

  protected function syncTags(array $tags) {
    // omitted for brevity.
  }
  
}

Note: I don't suffix the interactor classname with "Interactor" because I typically implement interactors as self-handling commands within Laravel. My App\Commands classes extend Deefour\Interactor\Interactor and retain all the functionality of normal Laravel commands, including the ability to be queued.

The PodcastRepository is a type-hinted parameter on the interactor instead of the context because it is a dependency of the interactor not directly containing information from a unique request to perform it's unit of work. Dependency resolution via a service container like Laravel's IoC container is supported by the deefour/interactor package.

Notice, the syncTags() can safely type-hint the $tags argument, because we are guaranteeing the tags pulled from the transformer will be an array.

Tying It All Together

Finally, the PodcastController is shown below with the relevant bits for this example.

<?php namespace App\Http\Controllers;

use App\Podcast;
use App\Contexts\UpdatePodcastContext;
use App\Repositories\PodcastRepository;
use App\Transformers\PodcastTransformer;
use Deefour\Authorizer\ProvidesAuthorization;
use Deefour\Interactor\DispatchesInteractors;
use Request;
use Response;

class PodcastController extends Controller {

  use DispatchesInteractors;
  use ProvidesAuthorization;

  /**
   * Persists changes to an existing podcast.
   *
   * @param  Podcast  $podcast
   * @param  Request  $request
   * @return Response
   */  
  public function update(Podcast $podcast, Request $request) {
    $this->authorize($podcast);

    $context = new UpdatePodcastContext(
      $podcast,
      new PodcastTransformer($request->all())
    );

    $this->dispatchInteractor(UpdatePodcast::class, $context);
    
    if ( ! $context->ok()) {
      return redirect()->route('podcasts.edit', compact('podcast'))
        ->withInput()->withErrors()
        ->with('flash.notice', 'Unexpected Error: ' . $context->status()->error());
    }
    
    return redirect()->route('podcasts.edit', compact('podcast'))
      ->with('flash.success', 'The changes have been saved');
  }
  
  /**
   * {@inheritdoc}
   */
  protected function currentUser() {
    return app('user') ?: new User;
  }

}

Implementation Notes:

  1. In a real application, the use of the inclusion of the traits and currentUser() method should be pushed up into the parent controller to make it available throughout the application.
  2. Validation is omitted from the example above for brevity.
  3. The currentUser() method is an implementation of an abstract method defined in the ProvidesAuthorization trait, serving as a framework-agnostic way to retrieve the currently-logged-in user.

Failure and redirection handling aside, the update() action is composed of only 3 lines of code. Actions stay similarly small for much more complex scenarios as well. Separating authorization, validation (again, omitted above), request data formatting, and business logic from the controller itself helps keep code clean, testable, and easier to maintain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment