Skip to content

Best Practices

Daniel Mason edited this page Sep 10, 2015 · 5 revisions

Best practices

These are tips to help you get the most out of Aye Aye. They are somewhat subjective, and likely to change over time to reflect new ways of using Aye Aye that the developers hadn't considered.

  1. Serialize
  2. Deserialize
  3. Generators
  4. Helpers

Serialize

Aye Aye provides an interface called \AyeAye\Formatter\Serialize that can be implemented on Classes you plan or returning from endpoints. This helps you control what properties can will be returned by returning an array.

Example
use \AyeAye\Formatter\Serializable;

class User implements Serializable
{
    protected $username;
    
    protected $passwordHash;
    
    public function ayeAyeSerialize()
    {
        return [
            'username' => $username,
        ];
    }
}

In this example, $username which we're ok returning to unknown consumers is given, but $passwordHash is not. It's important to note that serializing is done recursively. This prevents you from accidentally leaking $passwordHash by making User a property of another object that gets returned. However, this does make large, deep objects slower to format, and can error out if it's caught in a recursion.

Deserialize

Similar to telling Aye Aye how to serialise your data, you can also tell it how to deserialize it. Imagine you want to get a whole lot of data about a User or other object from a consumer. You could ask for each parameter individually, or you could ask for a user object, and implement the Deserializable interface.

use \AyeAye\Formatter\Deserializable;

class User implements Deserializable
{
    protected $username;
    
    protected $passwordHash;
    
    public function getUsername()
    {
        return $username;
    }
    
    public static function ayeAyeDeserialize(array $data = [])
    {
        $user = new static();
        if(array_key_exists('username', $data)) {
            $user->username = $data['username'];
        }
        return $user;
    }
}

Now you can tell the user to send you a user object instead:

public function postUserEndpoint(User $user)
{
    return $user->getUsername();
}

Which will work if there is a user array or user object in the request:

{
    "user" : {
        "username" : "Alice"
    }
}

Generators

As you will have noticed, Aye Aye always puts whatever you return from endpoints into a the data property of a response object. This ay not always be what you want, or you may want to add additional properties when using constraints like HATEOAS.

In this case you can make your endpoint a generator instead of a method. Generators allow you to return multiple times from a single method, they can return tuples that behave like array keys, and they will only run up to the next value before returning meaning you can restrict how much work they do.

When using this method to send data that is in addition to what was asked for, it is a best practice to return the desired data first.

Example
public function getActiveUsers()
{
    yield $this->getUserHelper()->getUsersWhere(['active' => 1]);    // First we yield the actual data
    yield 'links' => $this->getNavigationHelper()->userNavigation(); // Then we get additional helpful information
}

Not only does this add links to the base response object alongside data which will be where our active users are, but because we used a generator, if we need to use getActiveUsers() elsewhere within our service for consistency, we do not need to run the potentially expensive part of the generator that gets our navigation information.

A note by the author:
Despite its use in the example above, it's probably best to avoid HATEOAS. It's something of an an anti pattern, one that Aye Aye tries to solve. HATEOAS explains how the API works by returning documentation along with the data the consumer actually asked for. Aye Aye explains how the API works by documenting endpoints in separate calls directly to the controllers.

Helpers

In the exaple above, you will notice that to get the users, we first used the getUserHelper() method, and to get navigation information we used getNavigationHelper(). Doing this allows us to separate out responsibilities into replaceable classes. Our controllers should need to know whether Users come from a database, or a file, if they use a ORM package such as doctrine. This UserHelper can be reused in other classes and easily replaced later if necessary.

Example
class UserHelper
{
   ...
}
trait HasUserHelper
{

    private $helper;
    
    protected getUserHelper() {
        if(!$this->helper) {
            $this->helper = new UserHelper();
        }
    }

    protected setUserHelper(UserHelper $helper) {
        $this->helper = $helper;
    }
}
class User extends Controller
{
    use HasUserHelper;
    
    ...
}

Using this trait as an intermediary allows us to not only change the entire user helper class later, but we can use it to easily insert a mock object for unit testing.