-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Usage of Validator #26
Comments
also having some problem validating datetime. e.g.:
|
as well as self::isInt(self::stringtoInt()); |
Can you post a few examples you've got difficulties with? JSON API Validator parses a JSON API document and validates it looks like a valid one. Then it passes actual string values to validatation rules. As a developer you describe your rules in Now how rules work.
Can you post a few examples so I can help you with? |
i did some tests:
EOT; in SubmissionRules: i put the rules as:
and
and SubmissionApiTests throw exception as below:
) i just put your suggested rule in and now: the dateline exception gone |
for belongs to relationship i saw is the self::required compulsory? |
some updates: it was abit rare that, i re-run the test. with:
it throws: ErrorException: strlen() expects parameter 1 to be string, object given |
for your input {
"data" : {
"type" : "submissions",
"id" : null,
"attributes": {
"dateline": "2017-01-01T00:00:00+0000",
"present": "true"
}
}
} rules should be $typeRule = v::equals('submissions');
$idRule = v::equals(null);
$attrRules = [
'dateline' => v::stringToDateTime(DateTimeBaseType::JSON_API_FORMAT),
'present' => v::stringToBool(),
]; adding If you post an example for relationships I can help you with it as well |
that means if stringToXX present. isXXX is not required to be chained right? |
thanks @neomerx, the boolean and integer part were also solved with you guidance of using proper chaining. but the datetime part wasn't solved yet. |
|
<?php namespace App\Data\Models;
use Doctrine\DBAL\Types\Type;
use Limoncello\Contracts\Application\ModelInterface;
use Limoncello\Contracts\Data\RelationshipTypes;
use Limoncello\Flute\Types\JsonApiDateTimeType;
/**
* Class Submission
*
* @package App\Data\Models
*/
class Submission implements ModelInterface, CommonFields
{
/**
* Table name
*/
const TABLE_NAME = 'submissions';
/**
* Primary key
*/
const FIELD_ID = 'id_submission';
/**
* Foreign key
*/
const FIELD_ID_USER = User::FIELD_ID;
/**
* Field name
*/
const FIELD_DATELINE = 'dateline';
/**
* Field name
*/
const FIELD_PRESENT = 'present';
/**
* Field name
*/
const FIELD_DESCRIPTION = 'description';
/**
* Field name
*/
const FIELD_DISPLAY_ORDER = 'display_order';
/**
* Relationship name
*/
const REL_USER = 'user';
/**
* @inheritDoc
*/
public static function getTableName(): string
{
return static::TABLE_NAME;
}
/**
* @inheritDoc
*/
public static function getPrimaryKeyName(): string
{
return static::FIELD_ID;
}
/**
* @inheritDoc
*/
public static function getAttributeTypes(): array
{
return [
self::FIELD_ID => Type::INTEGER,
self::FIELD_ID_USER => Type::INTEGER,
self::FIELD_DATELINE => JsonApiDateTimeType::NAME,
self::FIELD_PRESENT => Type::BOOLEAN,
self::FIELD_DESCRIPTION => Type::TEXT,
self::FIELD_DISPLAY_ORDER => Type::INTEGER,
self::FIELD_CREATED_AT => JsonApiDateTimeType::NAME,
self::FIELD_UPDATED_AT => JsonApiDateTimeType::NAME,
self::FIELD_DELETED_AT => JsonApiDateTimeType::NAME,
];
}
/**
* @inheritDoc
*/
public static function getAttributeLengths(): array
{
return [];
}
/**
* @inheritDoc
*/
public static function getRelationships(): array
{
return [
RelationshipTypes::BELONGS_TO => [
self::REL_USER => [User::class, self::FIELD_ID_USER, User::REL_SUBMISSIONS],
],
];
}
} |
<?php namespace App\Json\Schemes;
use App\Data\Models\Submission as Model;
/**
* Class SubmissionScheme
*
* @package App\Json\Schemes
*/
class SubmissionScheme extends BaseScheme
{
/**
* Attribute type
*/
const TYPE = 'submissions';
/**
* Model class name
*/
const MODEL = Model::class;
/**
* Attribute name
*/
const ATTR_DATELINE = Model::FIELD_DATELINE;
/**
* Attribute name
*/
const ATTR_PRESENT = Model::FIELD_PRESENT;
/**
* Attribute name
*/
const ATTR_DESCRIPTION = Model::FIELD_DESCRIPTION;
/**
* Attribute name
*/
const ATTR_DISPLAY_ORDER = 'display-order';
/**
* Relationship name
*/
const REL_USER = Model::REL_USER;
/**
* @inheritDoc
*/
public static function getMappings(): array
{
return [
self::SCHEMA_ATTRIBUTES => [
self::ATTR_DATELINE => Model::FIELD_DATELINE,
self::ATTR_PRESENT => Model::FIELD_PRESENT,
self::ATTR_DESCRIPTION => Model::FIELD_DESCRIPTION,
self::ATTR_DISPLAY_ORDER => Model::FIELD_DISPLAY_ORDER,
self::ATTR_CREATED_AT => Model::FIELD_CREATED_AT,
self::ATTR_UPDATED_AT => Model::FIELD_UPDATED_AT,
self::ATTR_DELETED_AT => Model::FIELD_DELETED_AT,
],
self::SCHEMA_RELATIONSHIPS => [
self::REL_USER => Model::REL_USER,
],
];
}
/**
* @inheritDoc
*/
protected function getExcludesFromDefaultShowSelfLinkInRelationships(): array
{
return [
self::REL_USER => true,
];
}
/**
* @inheritDoc
*/
protected function getExcludesFromDefaultShowRelatedLinkInRelationships(): array
{
return [
self::REL_USER => true,
];
}
} |
And rules? |
<?php namespace App\Authorization;
use App\Data\Models\Submission as Model;
use App\Json\Api\SubmissionsApi as Api;
use App\Json\Schemes\SubmissionScheme as Scheme;
use Limoncello\Application\Contracts\Authorization\ResourceAuthorizationRulesInterface;
use Limoncello\Auth\Contracts\Authorization\PolicyInformation\ContextInterface;
use Limoncello\Flute\Contracts\FactoryInterface;
use Settings\Passport;
/**
* Class SubmissionRules
*
* @package App\Authorization
*/
class SubmissionRules implements ResourceAuthorizationRulesInterface
{
use RulesTrait;
/**
* Action name
*/
const ACTION_VIEW_SUBMISSIONS = 'canViewSubmissions';
/**
* Action name
*/
const ACTION_CREATE_SUBMISSIONS = 'canCreateSubmissions';
/**
* Action name
*/
const ACTION_EDIT_SUBMISSIONS = 'canEditSubmissions';
/**
* @inheritdoc
*/
public static function getResourcesType(): string
{
return Scheme::TYPE;
}
/**
* @param ContextInterface $context
*
* @return bool
*/
public static function canViewSubmissions(ContextInterface $context): bool
{
return self::hasScope($context, Passport::SCOPE_ADMIN_SUBMISSIONS) || self::hasScope($context, Passport::SCOPE_VIEW_SUBMISSIONS);
}
/**
* @param ContextInterface $context
*
* @return bool
*/
public static function canCreateSubmissions(ContextInterface $context): bool
{
return self::hasScope($context, Passport::SCOPE_ADMIN_SUBMISSIONS);
}
/**
* @param ContextInterface $context
*
* @return bool
*/
public static function canEditSubmissions(ContextInterface $context): bool
{
return self::hasScope($context, Passport::SCOPE_ADMIN_SUBMISSIONS);
}
/**
* @param ContextInterface $context
*
* @return bool
*/
private static function isSubmissionOwner(ContextInterface $context): bool
{
$isSubmissionOwner = false;
if (($userId = self::getCurrentUserIdentity($context)) !== null) {
$identity = self::reqGetResourceIdentity($context);
$container = self::ctxGetContainer($context);
$factory = $container->get(FactoryInterface::class);
$api = $factory->createApi(Api::class);
$submission = $api->readResource($identity);
$isSubmissionOwner = $submission !== null && $submission->{Model::FIELD_ID_USER} === $userId;
}
return $isSubmissionOwner;
}
} |
<?php
namespace App\Http\Controllers;
use App\Json\Api\SubmissionsApi as Api;
use App\Json\Schemes\SubmissionScheme as Scheme;
use App\Json\Validators\SubmissionCreate;
use App\Json\Validators\SubmissionUpdate;
/**
* Class SubmissionsController
*
* @package App\Http\Controllers
*/
class SubmissionsController extends BaseController
{
/**
* Api class name
*/
const API_CLASS = Api::class;
/**
* Schema class name
*/
const SCHEMA_CLASS = Scheme::class;
/**
* Validator class name
*/
const ON_CREATE_VALIDATION_RULES_SET_CLASS = SubmissionCreate::class;
/**
* Validator class name
*/
const ON_UPDATE_VALIDATION_RULES_SET_CLASS = SubmissionUpdate::class;
} |
The description field is mapped to LONGTEXT in mysql; <?php
namespace App\Json\Validators\Rules;
use App\Data\Models\Submission as Model;
use App\Json\Schemes\SubmissionScheme as Scheme;
use App\Json\Schemes\UserScheme;
use Limoncello\Flute\Types\DateTimeBaseType;
use Limoncello\Flute\Validation\Rules\ExistInDatabaseTrait;
use Limoncello\Flute\Validation\Rules\RelationshipsTrait;
use Limoncello\Validation\Contracts\Rules\RuleInterface;
use Limoncello\Validation\Rules;
use PHPMD\Rule;
/**
* Class SubmissionRules
*
* @package App\Json\Validators\Rules
*/
final class SubmissionRules extends Rules
{
use RelationshipsTrait, ExistInDatabaseTrait;
/**
* @return RuleInterface
*/
public static function isSubmissionType(): RuleInterface
{
return self::equals(Scheme::TYPE);
}
/**
* @return RuleInterface
*/
public static function isSubmissionId(): RuleInterface
{
return self::stringToInt(self::exists(Model::TABLE_NAME, Model::FIELD_ID));
}
/**
* @return RuleInterface
*/
public static function isUserRelationship(): RuleInterface
{
return self::toOneRelationship(UserScheme::TYPE, UserRules::isUserId());
}
/**
* @return RuleInterface
*/
public static function dateline(): RuleInterface
{
return self::stringToDateTime(DateTimeBaseType::JSON_API_FORMAT);
}
public static function present(): RuleInterface
{
return self::stringToBool();
}
/**
* @return RuleInterface
*/
public static function description(): RuleInterface
{
return self::isString();
}
/**
* @return RuleInterface
*/
public static function displayOrder(): RuleInterface
{
return self::stringToInt();
}
} |
so far seems fine. What about |
public function testCreate()
{
try {
$this->setPreventCommits();
$jsonInput = <<<EOT
{
"data" : {
"type" : "submissions",
"id" : null,
"attributes": {
"dateline": "2017-01-01T00:00:00+0800",
"present": "-1",
"display-order": "AAA"
},
"relationships": {
"user": {
"data": { "type": "users", "id": "55" }
}
}
}
}
EOT;
$headers = ['Authorization' => 'Bearer ' . $this->getAdministratorsOAuthToken()];
$response = $this->postJsonApi(self::API_URI, $jsonInput, $headers);
$this->assertEquals(201, $response->getStatusCode());
$json = json_decode((string)$response->getBody());
$this->assertObjectHasAttribute('data', $json);
$submissionId = $json->data->id;
$this->assertEquals(200, $this->get(self::API_URI . "/$submissionId", [], $headers)->getStatusCode());
$query = $this->getCapturedConnection()->createQueryBuilder();
$statement = $query
->select('*')
->from(Submission::TABLE_NAME)
->where(Submission::FIELD_ID . '=' . $query->createPositionalParameter($submissionId))
->execute();
$this->assertNotEmpty($values = $statement->fetch());
$this->assertNotEmpty($values[ Submission::FIELD_CREATED_AT ]);
} catch (JsonApiException $e) {
print_r($e->getErrors());
}
} |
<?php
namespace App\Json\Validators;
use App\Json\Schemes\SubmissionScheme as Scheme;
use App\Json\Validators\Rules\SubmissionRules as v;
use Limoncello\Flute\Contracts\Validation\JsonApiRuleSetInterface;
use Limoncello\Flute\Types\DateTimeBaseType;
use Limoncello\Validation\Contracts\Rules\RuleInterface;
/**
* Class SubmissionCreate
*
* @package App\Json\Validators
*/
class SubmissionCreate implements JsonApiRuleSetInterface
{
/**
* @inheritDoc
*/
public static function getTypeRule(): RuleInterface
{
return v::isSubmissionType();
}
/**
* @inheritDoc
*/
public static function getIdRule(): RuleInterface
{
return v::equals(null);
}
/**
* @inheritDoc
*/
public static function getAttributeRules(): array
{
return [
Scheme::ATTR_DATELINE => v::required(v::dateline()),
Scheme::ATTR_PRESENT => v::required(v::present()),
Scheme::ATTR_DESCRIPTION => v::description(),
Scheme::ATTR_DISPLAY_ORDER => v::displayOrder(),
];
}
/**
* @inheritDoc
*/
public static function getToOneRelationshipRules(): array
{
return [
Scheme::REL_USER => v::required(v::isUserRelationship()),
];
}
/**
* @inheritDoc
*/
public static function getToManyRelationshipRules(): array
{
return [];
}
} |
<?php
namespace App\Json\Validators;
use App\Json\Schemes\SubmissionScheme as Scheme;
use App\Json\Validators\Rules\SubmissionRules as v;
use Limoncello\Flute\Contracts\Validation\JsonApiRuleSetInterface;
use Limoncello\Validation\Contracts\Rules\RuleInterface;
/**
* Class SubmissionUpdate
*
* @package App\Json\Validators
*/
class SubmissionUpdate implements JsonApiRuleSetInterface
{
/**
* @inheritDoc
*/
public static function getTypeRule(): RuleInterface
{
return v::isSubmissionType();
}
/**
* @inheritDoc
*/
public static function getIdRule(): RuleInterface
{
return v::isSubmissionId();
}
/**
* @inheritDoc
*/
public static function getAttributeRules(): array
{
return [
Scheme::ATTR_DATELINE => v::dateline(),
Scheme::ATTR_PRESENT => v::present(),
Scheme::ATTR_DESCRIPTION => v::description(),
Scheme::ATTR_DISPLAY_ORDER => v::displayOrder(),
];
}
/**
* @inheritDoc
*/
public static function getToOneRelationshipRules(): array
{
return [
Scheme::REL_USER => v::isUserRelationship(),
];
}
/**
* @inheritDoc
*/
public static function getToManyRelationshipRules(): array
{
return [];
}
} |
i runned the test again, $jsonInput = <<<EOT
{
"data" : {
"type" : "submissions",
"id" : null,
"attributes": {
"dateline": "2017-01-01T00:00:00+0800",
"present": "-1",
"display-order": "AAA"
},
"relationships": {
"user": {
"data": { "type": "users", "id": "55" }
}
}
}
}
EOT; it was rare again, |
Can you provide stack for 'strlen() expects parameter 1 to be string, object given'? display-order will not produce any error. For PHP 'AAA' is a valid input to convert it to int. It can be checked for numeric but let's do it later. The 'strlen' error first. |
@neomerx let me try reproduce it. |
|
Aha, so it passes validation but fails on saving the value to the database... |
|
I think it should be an error in Can you try to replace it with the following? public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if ($value instanceof DateTimeInterface) {
$dateTime = $value;
} elseif ($value instanceof JsonApiDateTime) {
$dateTime = $value->getValue();
} elseif (is_string($value) === true) {
if (($dateTime = DateTime::createFromFormat(DateBaseType::JSON_API_FORMAT, $value)) === false) {
throw ConversionException::conversionFailed($value, $this->getName());
}
} else {
throw ConversionException::conversionFailed($value, $this->getName());
}
return parent::convertToDatabaseValue($dateTime, $platform);
} |
yes. replaced with your snippet and the issue gone |
as for display order try to add public static function displayOrder(): RuleInterface
{
return self::isNumeric(self::stringToInt());
} |
i have two more concerns. /**
* @return RuleInterface
*/
public static function description(): RuleInterface
{
return self::nullable(self::isString());
} in case of a integer with default value (or anything default numeric with default value) /**
* @return RuleInterface
*/
public static function displayOrder(): RuleInterface
{
return self::isNumeric(self::stringToInt());
} is it a proper way? for hasMany relationship |
If you don't use Built-in
|
public static function name(): RuleInterface
{
return self::orX(
self::exists(Model::TABLE_NAME, Model::FIELD_NAME),
self::stringLengthBetween(Model::MIN_NAME_LENGTH, Model::getAttributeLengths()[ Model::FIELD_NAME ])
);
} and /**
* @return RuleInterface
*/
public static function uniqueName(): RuleInterface
{
return self::andX(
self::unique(Model::TABLE_NAME, Model::FIELD_NAME),
self::stringLengthBetween(Model::MIN_NAME_LENGTH, Model::getAttributeLengths()[ Model::FIELD_NAME ])
);
} seems does validate properly in case of the following input: $jsonInput = <<<EOT
{
"data" : {
"type" : "lessons",
"id" : null,
"attributes" : {
"name" : "Mathematics",
"description" : "some description",
"display-order" : "0"
}
}
}
EOT; with minimum length of name in model: /**
* Minimum name length
*/
const MIN_NAME_LENGTH = '3'; and maximum length of name /**
* @inheritDoc
*/
public static function getAttributeLengths(): array
{
return [
self::FIELD_NAME => 100,
];
} in Validator "LessonCreate" public static function getAttributeRules(): array
{
return [
Scheme::ATTR_NAME => v::required(v::uniqueName()),
Scheme::ATTR_DESCRIPTION => v::description(),
Scheme::ATTR_DISPLAY_ORDER => v::displayOrder(),
];
} the test throws correctly . 1 / 1 (100%)object(Neomerx\JsonApi\Exceptions\ErrorCollection)#517 (1) {
["items":"Neomerx\JsonApi\Exceptions\ErrorCollection":private]=>
array(1) {
[0]=>
object(Neomerx\JsonApi\Document\Error)#518 (8) {
["idx":"Neomerx\JsonApi\Document\Error":private]=>
NULL
["links":"Neomerx\JsonApi\Document\Error":private]=>
NULL
["status":"Neomerx\JsonApi\Document\Error":private]=>
string(3) "422"
["code":"Neomerx\JsonApi\Document\Error":private]=>
NULL
["title":"Neomerx\JsonApi\Document\Error":private]=>
string(21) "The value is invalid."
["detail":"Neomerx\JsonApi\Document\Error":private]=>
string(40) "The value should be a unique identifier."
["source":"Neomerx\JsonApi\Document\Error":private]=>
array(1) {
["pointer"]=>
string(21) "/data/attributes/name"
}
["meta":"Neomerx\JsonApi\Document\Error":private]=>
NULL
}
}
} as "Mathematics" already exists it throws: . 1 / 1 (100%)object(Neomerx\JsonApi\Exceptions\ErrorCollection)#517 (1) {
["items":"Neomerx\JsonApi\Exceptions\ErrorCollection":private]=>
array(1) {
[0]=>
object(Neomerx\JsonApi\Document\Error)#518 (8) {
["idx":"Neomerx\JsonApi\Document\Error":private]=>
NULL
["links":"Neomerx\JsonApi\Document\Error":private]=>
NULL
["status":"Neomerx\JsonApi\Document\Error":private]=>
string(3) "422"
["code":"Neomerx\JsonApi\Document\Error":private]=>
NULL
["title":"Neomerx\JsonApi\Document\Error":private]=>
string(21) "The value is invalid."
["detail":"Neomerx\JsonApi\Document\Error":private]=>
string(49) "The value should be between 3 and 100 characters."
["source":"Neomerx\JsonApi\Document\Error":private]=>
array(1) {
["pointer"]=>
string(21) "/data/attributes/name"
}
["meta":"Neomerx\JsonApi\Document\Error":private]=>
NULL
}
}
} i think it would be too many questions today 📦 |
You've found another bug 🎉 In parent::__construct($min, $min, ErrorCodes::STRING_LENGTH_BETWEEN, $errorContext); should be replaced with parent::__construct($min, $max, ErrorCodes::STRING_LENGTH_BETWEEN, $errorContext); |
all fixes will be published later this week |
i have originally a scenario on validator:
for previous version of limoncello-php/app i used a dirty hack in BaseAppValidator; snippets: /**
* @return RuleInterface
*/
protected function name($index = null): RuleInterface
{
$primaryKey = $index === null ? $index : [Model::FIELD_ID, $index];
$maxLength = Model::getAttributeLengths()[Model::FIELD_NAME];
$name = $this->andX(
$this->stringLength(Model::MIN_NAME_LENGTH, $maxLength),
$this->isString()
);
return $this->andX($name, $this->isUnique(Model::TABLE_NAME, Model::FIELD_NAME, false, $primaryKey)); private function exists($tableName, $columnName, $value, $primaryKey = null): bool
{
/** @var Connection $connection */
$connection = $this->getContainer()->get(Connection::class);
if ($primaryKey === null) {
$query = $connection->createQueryBuilder();
$query
->select($columnName)
->from($tableName)
->where($columnName . '=' . $query->createPositionalParameter($value))
->setMaxResults(1);
$fetched = $query->execute()->fetch();
$result = $fetched !== false;
} else {
list($primaryKeyName, $primaryKeyValue) = $primaryKey;
$query = $connection->createQueryBuilder();
$query
->select("`{$primaryKeyName}`, `{$columnName}`")
->from($tableName)
->where($columnName . '=' . $query->createPositionalParameter($value))
->setMaxResults(1);
$fetched = $query->execute()->fetch();
$result = $fetched !== false &&
$fetched[$primaryKeyName] !== $primaryKeyValue;
}
return $result;
} on 0.7.x forward, i found this hack no longer usable, as i can't find the entry point for capturing the index (id) of record. |
so for As for a) Do not check uniqueness of the name on update. Intercept the captured data between Validation and API and make additional check (or add the check to API level) Currently, the easiest one would be option b).
|
i tried to do it in a custom validator like 'isUpdateUniqueRule' ya, i think option b would be easier for me to do |
b) is not a bad option it's just not universal. You can ping me at the end of the week I might have something for c). I'm working on implementing validation for the publishing process. This involves different rules depending on the input data, validation of combined input data and data stored in the database. I hope something good may come out of this for more generic use. |
the procedure would be:
am i correct? |
i mean , is there any convention on where to put my own custom class |
there are no special requirements for placing custom rules. You can put it near to custom rule example |
the option b works; though it was not really perfect at the moment. but i can really feel the power of container wise capability!!! |
@dreamsbond After some working on a bigger project I came to a conclusion that it's better to move some validator files around and structure them a bit different. It will provide a better manageability in the long run. Some new features in Validation: new rule As for custom validation (option c)) I've got some good news as well. In protected static function createOnCreateValidator(ContainerInterface $container): JsonApiValidatorInterface
{
$wrapper = new CustomOnCreateValidator (parent::createOnCreateValidator($container));
return $wrapper;
}
protected static function createOnUpdateValidator(ContainerInterface $container): JsonApiValidatorInterface
{
$wrapper = new CustomOnUpdateValidator (parent::createOnUpdateValidator($container));
return $wrapper;
} An example of custom validator wrapper could be seen here. |
@neomerx is it possible to have documentation for validator usage?
as i think i was not very familiar with the part doing conversion, e.g.: stringtoadateline, etc...
having some custom validation logic, but cant find entry point of gettings values from request and making it usable on validator
The text was updated successfully, but these errors were encountered: