Policies
Policies are essentially callbacks for fields that determine if the user is allowed to view that field based on the loaded data. For example, for a public API, you will probably want to hide certain data from unauthenticated requests, while other data might be visible. This page will go over creating such a policy. It will assume you have already read the Getting Started page.
Defining the schemas
We'll start with a single resource: users. A user usually has a bunch of sensitive data that should not be visible to just about anyone, such as their address, birth date, email, etc.
// File: /my_project/app/Schemas/UserSchema.php
<?php
namespace App\Schemas;
class UserSchema extends Schema
{
public function fields(): array
{
return [
'id' => $this->int('id'),
'name' => $this->string('name'),
'email' => $this->string('email'),
'birthdate' => $this->date('birthdate'),
'member_since' => $this->datetime('member_since'),
];
}
// filters(), sorts(), and model() omitted for brevity.
}
With the above schema, anyone can access any of the fields.
Writing a policy
To manage the visibility of certain data, we'll write a policy that defines that
the request must come from an authenticated request; that is, there is a logged
in user available on the Request
object.
// File: /my_project/app/Schemas/Policies/Authenticated.php
<?php
namespace App\Schema\Policies;
use Apitizer\Policies\Policy;
class Authenticated implements Policy
{
public function passes($value, $row, $fieldOrAssociation): bool
{
return !! $fieldOrAssociation->getSchema()->getRequest()->user();
}
}
The Authenticated
policy above fetches the request that is currently being
handled, and checks that there is a user on that request. We could have used the
request()
global helper that Laravel defines, but this makes it harder to test
if we're using custom Request
objects during testing.
Now that we have a policy, we can apply it to a field. In this case we'll state
that the email
, birthdate
, and member_since
fields require the user to be
logged in.
// File: /my_project/app/Schemas/UserSchema.php
<?php
namespace App\Schemas;
use App\Schemas\Policies\Authenticated;
class UserSchema extends Schema
{
public function fields(): array
{
return [
'id' => $this->int('id'),
'name' => $this->string('name'),
'email' => $this->string('email')
->policy(new Authenticated),
'birthdate' => $this->date('birthdate')
->policy(new Authenciated),
'member_since' => $this->datetime('member_since')
->policy(new Authenticated),
];
}
// filters(), sorts(), and model() omitted for brevity.
}
If you were to now issue an unauthenticated request with the following
selection: ?fields=id,name,email
, you would only get back the id
and the
name
, the email
field will be gone.
Multiple policies
A field may have more than one policy defined on it. The policy
method accepts
multiple Policy
objects to be given. For example:
public function fields(): array
{
return [
'email' => $this->string('email')
->policy(new Authenticated, new IsViewingThemselves)
];
}
In this scenario, the Authenticated
and IsViewingThemselves
policies must
both pass before the field as a whole passes. However, you might want to check
multiple policies for at least one that passes. For example, you might have an
IsAdmin
policy that, if it passes, should ignore all the other policies. For
these use cases, there is the policyAny
method that accepts a list of policies
and checks, in order, for the first policy that passes. If none pass, the policy
check fails:
public function fields(): array
{
return [
'email' => $this->string('email')
->policyAny(new IsAdmin, new IsViewingThemselves)
];
}
The policy
and policyAny
function can also be chained.
Policies on associations
Policies on associations function exactly the same as policies on fields: the policy will receive all the data from that association. If the policy fails, the association will not be rendered.
The data the policy receives will be the same as directly accessing the relation on the model:
$user->posts // hasMany
$post->author // belongsTo
No distinction is made based on the number of rows that an association returns.
Caching expensive policies
As you might have already noticed, defining new policies for each field might be
slow if the policy itself is an expensive operation. For example, if the
Authenticated
policy from the "Writing a policy" section used multiple
database calls it would take unnecessarily long if the policy was called for all
fields that use that policy. In this case, the policy itself does not use the
field value (first parameter to the passes
method in a Policy) at all and can
be applied to any field. That also means that whichever field uses the policy
will get the same result. The Authenticated
policy can therefore be easily
cached. Luckily, Apitizer
already defines a helper for this: CachedPolicy
:
// File: /my_project/app/Schemas/UserSchema.php
<?php
namespace App\Schemas;
use App\Schemas\Policies\Authenticated;
use Apitizer\Policies\CachedPolicy;
class UserSchema extends Schema
{
public function fields(): array
{
$policy = new CachedPolicy(new Authenticated);
return [
'id' => $this->int('id'),
'name' => $this->string('name'),
'email' => $this->string('email')->policy($policy),
'birthdate' => $this->date('birthdate')->policy($policy),
'member_since' => $this->datetime('member_since')->policy($policy),
];
}
// filters(), sorts(), and model() omitted for brevity.
}
With our new cached policy in place, the Authenticated
policy will only be
called once, regardless of how many fields use the policy.
Ensuring data is fetched
Some policies might be dependent on certain data being available in order to
pass. For example, a policy that checks if the row of data belongs to a user
using a user_id
column, is dependent on this column being present. However, if
the client never requested this column, or if the column is not available to the
user to begin with, the policy would never pass. To solve this, the schema has
an alwaysLoadColumns
property:
class PostSchema extends Schema
{
protected $alwaysLoadColumns = ['author_id'];
}
This ensures that the author_id
is always loaded by the schema, making
it available to use in policies.