Here's a call I'm making:
PATCH /api/offers/<id>
{
"data": {
"type": "offers",
"id": "<id>",
"attributes": {
"rate-offered": 410
},
"relationships": {
"dates": {
"data": [
{ "type": "dates", "id": "4D7CB185-944A-473A-9ADA-0E185B92BC1F" },
{ "type": "dates", "id": "DB8367B7-1194-4B35-ADD6-5439B3235180" }
]
}
}
}
}
How would I validate the dates in the relationship. For example, I want to ensure all the dates are 'contiguous'.
All I can see with regards to relationship validation is if it's required or is a hasOne/hasMany etc.
Cheers,
Dan
Hi!
Yes, this is possible at the moment, though it does need to be made easier.
You want to provide a context validator by overloading this method in your Validators class:
https://github.com/cloudcreativity/laravel-json-api/blob/master/src/Validators/AbstractValidatorProvider.php#L318-L334
As the doc block says, if you return a validator that implements the resource validator interface, this validator will be used once the whole resource has been validated, and allows you access to any part of it. I.e. you'll know the resource object has the dates relationship and then you can inspect the ids that have been provided. Let me know if you have any questions on doing this. The one tip I can give is to use this trait for the errors on your validator:
https://github.com/cloudcreativity/json-api/blob/master/src/Utils/ErrorsAwareTrait.php
I'm planning on improving the relationships validators - making them a lot more fluent - and need to make this easier to do, so will leave this issue open as a reminder.
Fab - thanks!
Just as a side issue to this, what's the difference between ErrorsAwareTrait and ErrorCreatorTrait? I know the abstract authorizer uses the latter (and thus makes its methods available in my authorizer classes), but I don't really see what the difference is.
They seem to both require errors to be added the same way - e.g.
$this->addError(Error::create([
'status' => 403,
'title' => __('Invalid user type'),
'detail' => __('The authenticated user type must be either a locum or practice employee.'),
]));
ErrorCreatorTrait allows you to add an error using a string - which is the string key from your errors config in your api config. You'd have to implement the following abstract method:
protected function getErrorRepository()
{
return json_api()->getErrors();
}
I need to remove one of them at some point so that there's only one of them... it's really not high up on my to-do list though!
Hi, just adding my two cents to this subject... I am looking to validate a "unique with" pattern (basically the same as a two-column "UNIQUE" index in SQL) where one column is a relationship ID. The attributes validator doesn't have access to the relationships and the relationships validator doesn't have access to the attributes. This is a little different than native Laravel which tends to flatten these together. So I have run in to difficulty with validating this case. I think the solution path discussed herein is doable but is a little bit dirty, though I understand that everyone agrees and also I understand that real life calls so I am not expecting free work from anybody.
So, for example, for each team with "team_id" made up of multiple users, each user "title" must be unique, however different teams can have identical titles within each of them.
So, in my case I am storing the validation rules on the Eloquent models, and I am using getter methods to pass them to the API validator. The model also validates itself before saving to the database in order to protect from programming errors (for now). This is sorta-kinda-maybe similar to how Ruby on Rails handles validation. It's just a super handy pattern so I am sharing it here in case it informs future API validation design.
I'm using this package to manage the on-model validation:
https://github.com/dwightwatson/validating
Another similar package which is slick but doesn't quite support Laravel 5.5 yet and is undergoing splitting the package apart to separate features to improve future Laravel updates support.
https://github.com/jarektkaczyk/eloquence
On-model rules look like this:
class Project extends BaseModel
{
// Validation trait is included in project-global "BaseModel"
protected $rules = [
'id' => ['integer', 'min:200000', 'digits:6', 'unique:projects,id', 'nullable'],
'name' => ['bail', 'required', 'string', 'between:6,250', 'unique:projects'],
'project_type_id' => ['bail', 'required', 'integer', 'min:1', 'exists:project_types,id'],
'office_id' => ['bail', 'required', 'integer', 'min:1', 'exists:offices,id'],
'project_status_id' => ['bail', 'required', 'integer', 'min:1', 'exists:project_statuses,id'],
'client_company_id' => ['integer', 'min:1', 'exists:client_companies,id', 'nullable'],
];
}
And my template API validator, extended by all of my API model definitions, which grabs the model validation rules (which adds self-referencing ID number in update case) and strips out validation on relationship ID fields (which all end with '_id').
<?php
namespace App\JsonApi;
use App\Models\LocalModel;
use CloudCreativity\LaravelJsonApi\Validators\AbstractValidatorProvider;
use Config;
abstract class TemplateValidators extends AbstractValidatorProvider
{
const RELATIONSHIP_ID_SUFFIX = '_id';
/**
* @var array
*/
protected $queryRules = [
'page.number' => 'integer|min:1',
'page.size' => 'integer|between:1,100',
];
/**
* Get the validation rules for the resource attributes.
*
* @param LocalModel $record
* the record being updated, or null if it is a create request.
* @return array
*/
protected function attributeRules($record = null)
{
return $this->getFilteredAttributeRules($record);
}
/**
* Get the standard validation rules for the resource attributes.
*
* @param LocalModel $record
* the record being updated, or null if it is a create request.
* @return array
*/
protected function getFilteredAttributeRules($record = null)
{
return static::removeIdFieldsFromRules($this->getRawAttributeRules($record));
}
/**
* Get the model validation rules.
*
* @param LocalModel $record
* the record being updated, or null if it is a create request.
* @return array
*/
protected function getRawAttributeRules($record = null)
{
return $record ? $this->getResourceUpdateRules($record) : $this->getResourceCreateRules();
}
protected function getResourceClassName($type)
{
return Config::get('json-api-v1.resources.' . $type);
}
/**
* Get the model validation rules.
*
* @param LocalModel $record
* the record being updated, or null if it is a create request.
* @return array
*/
protected function getResourceUpdateRules($record)
{
$record->updateRulesUniques();
return $record->getRules();
}
protected function getResourceCreateRules()
{
return $this->getResourceClassName($this->getResourceType())::rules();
}
protected static function removeIdFieldsFromRules($rules)
{
return array_filter($rules, function($key) {
return !static::strEndsWith($key, static::RELATIONSHIP_ID_SUFFIX);
}, ARRAY_FILTER_USE_KEY);
}
protected static function strEndsWith($haystack, $needle)
{
$length = strlen($needle);
return $length === 0 ||
(substr($haystack, -$length) === $needle);
}
}
A validation rule with "unique with" looks like this (using this validator: https://github.com/felixkiss/uniquewith-validator)
protected $rules = [
//
'title' => ['string', 'min:3', 'max:20', 'alpha_num', 'unique_with:users,team_id', 'nullable'],
];
So, I am think about a clever way to satisfy this validation test programatically in all cases where an ID field reference exists. I am not opposed to writing my own validation tests to satisfy this, since most built-in validation tests don't accept direct injection of the test value, and expect the named field to exist in the list of provided attributes.
So we just do this using the context validator - i.e. perform validation of attributes that are dependent on relationships after the attributes and relationships have been validated. This has been working fine for us without it being too complex.
The main challenge with all of this is mapping Laravel validation messages back to the correct JSON API error pointer to identify where in the structure the error has originated from.
Saying that, the validation stuff is the first thing I wrote (now quite a while ago) and could definitely be improved.
Perhaps as an improvement, we could add the relationships data to the data that is passed to the Laravel attributes validator?
That way your rules example would become:
protected $rules = [
//
'title' => ['string', 'min:3', 'max:20', 'alpha_num', 'unique_with:users,relationships.team.data.id', 'nullable'],
];
Does that dot syntax for the unique_with rule actually work in Laravel?
Or alternatively, the Laravel validator could be given both the attributes and the relationships, so the rules would be:
protected $rules = [
//
'attributes.title' => ['string', 'min:3', 'max:20', 'alpha_num', 'unique_with:users,relationships.team.data.id', 'nullable'],
];
Yeah that does seem like a good approach. If possible, it would be better not require the "data" property in the relationship, I know that requires a behind-the-scenes hack but really the programmer does not gain any value from typing out "data" 100 times. You could also make it so that a singular field name without any period characters in it implies an attribute, ie. "attributes.title" and "title" have the same validation outcome. Among other reasons this would provide backward compatibility.
At the moment I am not sure how to use the context validator because it seems that it requires a new "ResourceValidator" to be initialized somehow. But, I'll poke around with it and post back here if I figure it out.
Just an update for others following this... here is my set up for the moment, which is sloppy for now but produces a functional validation result.
I am storing validator rules on my models. Take this Device model for instance:
protected $rules = [
'building_id' => ['bail', 'required', 'integer', 'exists:buildings,id'],
'device_type_id' => ['bail', 'required', 'integer', 'exists:device_types,id'],
'designation' => ['string', 'max:32', 'alpha_num', 'unique_with:devices,building_id'],
'group' => ['string', 'max:96', 'nullable'],
'licence' => ['string', 'min:3', 'max:12', 'alpha_num', 'unique_with:devices,building_id', 'nullable'],
];
This includes a non-standard "unique_with" rule which is basically checking for a UNIQUE SQL index integrity violation. Here it is comparing a JSON API attribute against a relationship ID.
In the validator for my Devices model I have included the following method:
protected function resourceContext($record = null)
{
return new ContextValidator;
}
The ContextValidator is as follows:
<?php
namespace app\JsonApi\Devices;
use App\Models\Building\Device;
use App\Models\LocalModel;
use CloudCreativity\JsonApi\Contracts\Object\ResourceObjectInterface;
use CloudCreativity\JsonApi\Contracts\Validators\ResourceValidatorInterface;
use CloudCreativity\JsonApi\Exceptions\RuntimeException;
use CloudCreativity\JsonApi\Repositories\ErrorRepository;
use CloudCreativity\JsonApi\Utils\ErrorsAwareTrait;
use CloudCreativity\LaravelJsonApi\Validators\ValidatorErrorFactory;
use Validator;
class ContextValidator implements ResourceValidatorInterface
{
use ErrorsAwareTrait;
/**
* @param ResourceObjectInterface $resource
* @param LocalModel|null $record
* the domain object that the resource represents.
* @return bool
*/
public function isValid(ResourceObjectInterface $resource, $record = null)
{
try {
$data['building_id'] = $resource->getRelationships()->getRelationship('building')->getData()->getId();
} catch(RuntimeException $e) {
$data['building_id'] = $record ? $record->getAttribute('building_id') : null;
}
$data['licence'] = $resource->getAttributes()->get('licence', $record->licence ?? null);
$data['designation'] = $resource->getAttributes()->get('designation', $record->designation ?? null);
$rules = array_intersect_key($record ? $this->getResourceUpdateRules($record) : Device::rules(), array_flip(['licence', 'designation']));
$validator = Validator::make($data, $rules);
if ($validator->passes()) {
return true;
}
$messages = $validator->getMessageBag();
$this->addErrors((new ValidatorErrorFactory(new ErrorRepository))->resourceInvalidAttributesMessages($messages));
return false;
}
/**
* Get the model validation rules.
*
* @param LocalModel $record
* the record being updated, or null if it is a create request.
* @return array
*/
protected function getResourceUpdateRules($record)
{
$record->updateRulesUniques();
return $record->getRules();
}
}
So what I am doing here is:
Note the really janky technique I am using for getting error messages. I want to use Laravel's built-in validation messaging, in part because I will later be requiring multilingual support. I have basically hijacked the error message generator used when the API is validating attributes. It's not very clean, but it works.
So I am planning to work on this to look at the validation rules, figure out which of the attribute tests are referring to relationships, and then belay those tests to be performed in the context validator space.
I'm going to document the new validation implementation, but given this request JSON:
{
"data": {
"type": "posts",
"id": "123",
"attributes": {
"title": "Hello World",
"content": "..."
},
"relationships": {
"author": {
"data": {
"type": "users",
"id": "99"
}
},
"tags": {
"data": [
{
"type": "tags",
"id": "1"
},
{
"type": "tags",
"id": "2"
}
]
}
}
}
}
The following array is passed as the data for validation:
$data = [
'type' => 'posts',
'id' => '123',
'title' => 'Hello World',
'content' => '...',
'author' => ['type' => 'users', 'id' => '99'],
'tags' => [
['type' => 'tags', 'id' => '1'],
['type' => 'tags', 'id' => '2']
]
];
This is possible because attributes and relationships members share a common namespace known as resource fields. So in other words, we pass the resource fields to the validator, not just the attributes.
That looks really slick, nice!
Most helpful comment
Hi!
Yes, this is possible at the moment, though it does need to be made easier.
You want to provide a context validator by overloading this method in your Validators class:
https://github.com/cloudcreativity/laravel-json-api/blob/master/src/Validators/AbstractValidatorProvider.php#L318-L334
As the doc block says, if you return a validator that implements the resource validator interface, this validator will be used once the whole resource has been validated, and allows you access to any part of it. I.e. you'll know the resource object has the dates relationship and then you can inspect the ids that have been provided. Let me know if you have any questions on doing this. The one tip I can give is to use this trait for the errors on your validator:
https://github.com/cloudcreativity/json-api/blob/master/src/Utils/ErrorsAwareTrait.php
I'm planning on improving the relationships validators - making them a lot more fluent - and need to make this easier to do, so will leave this issue open as a reminder.