-
Notifications
You must be signed in to change notification settings - Fork 8
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.
- Serialize
- Deserialize
- Generators
- Helpers
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.
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.
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"
}
}
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.
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.
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.
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.