Skip to content
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

Open
dreamsbond opened this issue Aug 22, 2017 · 43 comments
Open

Usage of Validator #26

dreamsbond opened this issue Aug 22, 2017 · 43 comments
Assignees

Comments

@dreamsbond
Copy link

dreamsbond commented Aug 22, 2017

@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

@dreamsbond
Copy link
Author

also having some problem validating datetime.

e.g.:
self::isDateTime(self::stringToDateTime(DateTimeBaseType::JSON_API_FORMAT));
with ISO8601 dateformat e.g "2016-10-01T00:00:00+0000" fails in test and return

[items:Neomerx\JsonApi\Exceptions\ErrorCollection:private] => Array
    (
        [0] => Neomerx\JsonApi\Document\Error Object
            (
                [idx:Neomerx\JsonApi\Document\Error:private] =>
                [links:Neomerx\JsonApi\Document\Error:private] =>
                [status:Neomerx\JsonApi\Document\Error:private] => 422
                [code:Neomerx\JsonApi\Document\Error:private] =>
                [title:Neomerx\JsonApi\Document\Error:private] => The value is invalid.
                [detail:Neomerx\JsonApi\Document\Error:private] => The value should be a valid date time.
                [source:Neomerx\JsonApi\Document\Error:private] => Array
                    (
                        [pointer] => /data/attributes/dateline
                    )

                [meta:Neomerx\JsonApi\Document\Error:private] =>
            )

    )

@dreamsbond
Copy link
Author

as well as self::isInt(self::stringtoInt());

@neomerx neomerx self-assigned this Aug 22, 2017
@neomerx
Copy link
Collaborator

neomerx commented Aug 22, 2017

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 JsonApiRuleSetInterface which contains rules for such JSON API document parts as id, type, individual attributes and individual relationships.

Now how rules work.

  1. You work with dates
    Expected value string
    v::stringToDateTime(DateTimeBaseType::JSON_API_FORMAT)
    it will check input value as a string and will convert it to a date time with the format given

  2. Rule chaining
    Rules support chaining one rule with another. Let's say you can convert a string to int time and then check its value is within a specified range.
    v::stringtoInt(v::between(5, 10))
    The first rule has a string as an input and the second one will have int.
    So when you write self::isInt(self::stringtoInt()) you firstly check string is int (error).

Can you post a few examples so I can help you with?

@dreamsbond
Copy link
Author

i did some tests:
with json input as below:

        $jsonInput = <<<EOT
    {
        "data" : {
            "type"  : "submissions",
            "id"    : null,
            "attributes": {
                "dateline": "2017-01-01T00:00:00+0000",
                "present": "true"
            }
        }
    }

EOT;

in SubmissionRules:

i put the rules as:

/**
 * @return RuleInterface
 */
public static function dateline(): RuleInterface
{
    return self::isDateTime(self::stringToDateTime(DateTimeBaseType::JSON_API_FORMAT));
}

and

/**
 * @return RuleInterface
 */
public static function present(): RuleInterface
{
    return self::isBool(self::stringToBool());
}

and SubmissionApiTests throw exception as below:
. 1 / 1 (100%)Neomerx\JsonApi\Exceptions\ErrorCollection Object
(
[items:Neomerx\JsonApi\Exceptions\ErrorCollection:private] => Array
(
[0] => Neomerx\JsonApi\Document\Error Object
(
[idx:Neomerx\JsonApi\Document\Error:private] =>
[links:Neomerx\JsonApi\Document\Error:private] =>
[status:Neomerx\JsonApi\Document\Error:private] => 422
[code:Neomerx\JsonApi\Document\Error:private] =>
[title:Neomerx\JsonApi\Document\Error:private] => The value is invalid.
[detail:Neomerx\JsonApi\Document\Error:private] => The value should be a valid date time.
[source:Neomerx\JsonApi\Document\Error:private] => Array
(
[pointer] => /data/attributes/dateline
)

                [meta:Neomerx\JsonApi\Document\Error:private] =>
            )

        [1] => Neomerx\JsonApi\Document\Error Object
            (
                [idx:Neomerx\JsonApi\Document\Error:private] =>
                [links:Neomerx\JsonApi\Document\Error:private] =>
                [status:Neomerx\JsonApi\Document\Error:private] => 422
                [code:Neomerx\JsonApi\Document\Error:private] =>
                [title:Neomerx\JsonApi\Document\Error:private] => The value is invalid.
                [detail:Neomerx\JsonApi\Document\Error:private] => The value should be a boolean.
                [source:Neomerx\JsonApi\Document\Error:private] => Array
                    (
                        [pointer] => /data/attributes/archive
                    )

                [meta:Neomerx\JsonApi\Document\Error:private] =>
            )

    )

)

i just put your suggested rule in and now:

the dateline exception gone
present remain throwing exception

@dreamsbond
Copy link
Author

dreamsbond commented Aug 22, 2017

for belongs to relationship

i saw
return self::required(self::toOneRelationship(BoardScheme::TYPE, BoardRules::isBoardId()));

is the self::required compulsory?

@dreamsbond
Copy link
Author

some updates:

it was abit rare that, i re-run the test.

with:

/**
 * @return RuleInterface
 */
public static function dateline(): RuleInterface
{
    return self::stringToDateTime(DateTimeBaseType::JSON_API_FORMAT);
}

it throws:

ErrorException: strlen() expects parameter 1 to be string, object given

@dreamsbond dreamsbond changed the title Usage of Validator (Question) Usage of Validator Aug 22, 2017
@neomerx
Copy link
Collaborator

neomerx commented Aug 22, 2017

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 v::requied makes input field mandatory (will produce an error if not given)

If you post an example for relationships I can help you with it as well

@dreamsbond
Copy link
Author

dreamsbond commented Aug 22, 2017

that means if stringToXX present. isXXX is not required to be chained right?

@dreamsbond
Copy link
Author

dreamsbond commented Aug 22, 2017

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.
it keep throwing "ErrorException: strlen() expects parameter 1 to be string, object given"

@neomerx
Copy link
Collaborator

neomerx commented Aug 22, 2017

isXXX almost never needed for JSON API inputs. It's for other validators. In JSON API you've got strings as inputs so after stringToXXX you always have the correct type.
As for stringToDateTime I don't see it uses strlen. Can you please provide more details in input data and the rule itself?

@dreamsbond
Copy link
Author

dreamsbond commented Aug 22, 2017

<?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],
            ],
        ];
    }
}

@dreamsbond
Copy link
Author

dreamsbond commented Aug 22, 2017

<?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,
        ];
    }

}

@limoncello-php limoncello-php deleted a comment from dreamsbond Aug 22, 2017
@neomerx
Copy link
Collaborator

neomerx commented Aug 22, 2017

And rules?

@dreamsbond
Copy link
Author

dreamsbond commented Aug 22, 2017

<?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;
    }
}

@dreamsbond
Copy link
Author

<?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;
}

@dreamsbond
Copy link
Author

dreamsbond commented Aug 22, 2017

The description field is mapped to LONGTEXT in mysql;
i used isString() for validation

<?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();
    }
}

@neomerx
Copy link
Collaborator

neomerx commented Aug 22, 2017

so far seems fine. What about SubmissionCreate and SubmissionUpdate?

@dreamsbond
Copy link
Author

dreamsbond commented Aug 22, 2017

  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());
        }
    }

@dreamsbond
Copy link
Author

dreamsbond commented Aug 22, 2017

<?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 [];
    }
}

@dreamsbond
Copy link
Author

<?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 [];
    }
}

@dreamsbond
Copy link
Author

dreamsbond commented Aug 22, 2017

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,
the dateline doesn't throw "ErrorException: strlen() expects parameter 1 to be string, object given" now.
also, as you see the "display-order" should throw exception but it doesn't

@neomerx
Copy link
Collaborator

neomerx commented Aug 22, 2017

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.

@dreamsbond
Copy link
Author

@neomerx let me try reproduce it.

@dreamsbond
Copy link
Author

There was 1 error:

1) Tests\SubmissionApiTest::testCreate
ErrorException: strlen() expects parameter 1 to be string, object given

/timable/limoncello-php/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/ConversionException.php:44
/timable/limoncello-php/vendor/limoncello-php/flute/src/Types/JsonApiDateTimeType.php:53
/timable/limoncello-php/vendor/limoncello-php/flute/src/Adapters/Repository.php:140
/timable/limoncello-php/vendor/limoncello-php/flute/src/Api/Crud.php:320
/timable/limoncello-php/app/Json/Api/SubmissionsApi.php:100
/timable/limoncello-php/vendor/limoncello-php/flute/src/Http/BaseController.php:112
/timable/limoncello-php/vendor/limoncello-php/core/src/Application/Application.php:285
/timable/limoncello-php/vendor/limoncello-php/core/src/Application/Application.php:351
/timable/limoncello-php/app/Json/Exceptions/ApiHandler.php:35
/timable/limoncello-php/vendor/limoncello-php/core/src/Application/Application.php:423
/timable/limoncello-php/vendor/limoncello-php/passport/src/Authentication/PassportMiddleware.php:66
/timable/limoncello-php/vendor/limoncello-php/core/src/Application/Application.php:423
/timable/limoncello-php/vendor/limoncello-php/application/src/Packages/Cors/CorsMiddleware.php:53
/timable/limoncello-php/vendor/limoncello-php/core/src/Application/Application.php:423
/timable/limoncello-php/vendor/limoncello-php/core/src/Application/Application.php:186
/timable/limoncello-php/vendor/limoncello-php/testing/src/ApplicationWrapperTrait.php:160
/timable/limoncello-php/vendor/limoncello-php/core/src/Application/Application.php:146
/timable/limoncello-php/vendor/limoncello-php/testing/src/TestCaseTrait.php:106
/timable/limoncello-php/vendor/limoncello-php/testing/src/JsonApiCallsTrait.php:47
/timable/limoncello-php/tests/SubmissionApiTest.php:115

ERRORS!

@neomerx
Copy link
Collaborator

neomerx commented Aug 22, 2017

Aha, so it passes validation but fails on saving the value to the database...

@dreamsbond
Copy link
Author

dreamsbond commented Aug 22, 2017

.                                                                   1 / 1 (100%)string(6038) "#0 [internal function]: Limoncello\Application\Packages\Application\Application->Limoncello\Application\Packages\Application\{closure}(2, 'strlen() expect...', '/timable/limonc...', 44, Array)
#1 /timable/limoncello-php/vendor/doctrine/dbal/lib/Doctrine/DBAL/Types/ConversionException.php(44): strlen(Object(DateTimeImmutable))
#2 /timable/limoncello-php/vendor/limoncello-php/flute/src/Types/JsonApiDateTimeType.php(53): Doctrine\DBAL\Types\ConversionException::conversionFailed(Object(DateTimeImmutable), 'datetime')
#3 /timable/limoncello-php/vendor/limoncello-php/flute/src/Adapters/Repository.php(140): Limoncello\Flute\Types\JsonApiDateTimeType->convertToDatabaseValue(Object(DateTimeImmutable), Object(Doctrine\DBAL\Platforms\MySQL57Platform))
#4 /timable/limoncello-php/vendor/limoncello-php/flute/src/Api/Crud.php(320): Limoncello\Flute\Adapters\Repository->create('App\\Data\\Models...', Array)
#5 /timable/limoncello-php/app/Json/Api/SubmissionsApi.php(100): Limoncello\Flute\Api\Crud->create(NULL, Array, Array)
#6 /timable/limoncello-php/vendor/limoncello-php/flute/src/Http/BaseController.php(112): App\Json\Api\SubmissionsApi->create(NULL, Array, Array)
#7 [internal function]: Limoncello\Flute\Http\BaseController::create(Array, Object(Limoncello\Container\Container), Object(Zend\Diactoros\ServerRequest))
#8 /timable/limoncello-php/vendor/limoncello-php/core/src/Application/Application.php(285): call_user_func(Array, Array, Object(Limoncello\Container\Container), Object(Zend\Diactoros\ServerRequest))
#9 /timable/limoncello-php/vendor/limoncello-php/core/src/Application/Application.php(351): Limoncello\Core\Application\Application->callHandler(Array, Array, Object(Limoncello\Container\Container), Object(Zend\Diactoros\ServerRequest))
#10 /timable/limoncello-php/app/Json/Exceptions/ApiHandler.php(35): Limoncello\Core\Application\Application->Limoncello\Core\Application\{closure}(Object(Zend\Diactoros\ServerRequest))
#11 [internal function]: App\Json\Exceptions\ApiHandler::handle(Object(Zend\Diactoros\ServerRequest), Object(Closure), Object(Limoncello\Container\Container))
#12 /timable/limoncello-php/vendor/limoncello-php/core/src/Application/Application.php(423): call_user_func(Array, Object(Zend\Diactoros\ServerRequest), Object(Closure), Object(Limoncello\Container\Container))
#13 /timable/limoncello-php/vendor/limoncello-php/passport/src/Authentication/PassportMiddleware.php(66): Limoncello\Core\Application\Application->Limoncello\Core\Application\{closure}(Object(Zend\Diactoros\ServerRequest))
#14 [internal function]: Limoncello\Passport\Authentication\PassportMiddleware::handle(Object(Zend\Diactoros\ServerRequest), Object(Closure), Object(Limoncello\Container\Container))
#15 /timable/limoncello-php/vendor/limoncello-php/core/src/Application/Application.php(423): call_user_func(Array, Object(Zend\Diactoros\ServerRequest), Object(Closure), Object(Limoncello\Container\Container))
#16 /timable/limoncello-php/vendor/limoncello-php/application/src/Packages/Cors/CorsMiddleware.php(53): Limoncello\Core\Application\Application->Limoncello\Core\Application\{closure}(Object(Zend\Diactoros\ServerRequest))
#17 [internal function]: Limoncello\Application\Packages\Cors\CorsMiddleware::handle(Object(Zend\Diactoros\ServerRequest), Object(Closure), Object(Limoncello\Container\Container))
#18 /timable/limoncello-php/vendor/limoncello-php/core/src/Application/Application.php(423): call_user_func(Array, Object(Zend\Diactoros\ServerRequest), Object(Closure), Object(Limoncello\Container\Container))
#19 [internal function]: Limoncello\Core\Application\Application->Limoncello\Core\Application\{closure}(Object(Zend\Diactoros\ServerRequest))
#20 /timable/limoncello-php/vendor/limoncello-php/core/src/Application/Application.php(186): call_user_func(Object(Closure), Object(Zend\Diactoros\ServerRequest))
#21 /timable/limoncello-php/vendor/limoncello-php/testing/src/ApplicationWrapperTrait.php(160): Limoncello\Core\Application\Application->handleRequest(Object(Closure), Object(Zend\Diactoros\ServerRequest))
#22 /timable/limoncello-php/vendor/limoncello-php/core/src/Application/Application.php(146): class@anonymous->handleRequest(Object(Closure), Object(Zend\Diactoros\ServerRequest))
#23 /timable/limoncello-php/vendor/limoncello-php/testing/src/TestCaseTrait.php(106): Limoncello\Core\Application\Application->run()
#24 /timable/limoncello-php/vendor/limoncello-php/testing/src/JsonApiCallsTrait.php(47): Tests\TestCase->call('POST', '/api/v1/submiss...', Array, Array, Array, Array, Array, Array, Object(Zend\Diactoros\Stream))
#25 /timable/limoncello-php/tests/SubmissionApiTest.php(116): Tests\SubmissionApiTest->postJsonApi('/api/v1/submiss...', '        {\n     ...', Array)
#26 [internal function]: Tests\SubmissionApiTest->testCreate()
#27 /timable/limoncello-php/vendor/phpunit/phpunit/src/Framework/TestCase.php(1069): ReflectionMethod->invokeArgs(Object(Tests\SubmissionApiTest), Array)
#28 /timable/limoncello-php/vendor/phpunit/phpunit/src/Framework/TestCase.php(928): PHPUnit\Framework\TestCase->runTest()
#29 /timable/limoncello-php/vendor/phpunit/phpunit/src/Framework/TestResult.php(695): PHPUnit\Framework\TestCase->runBare()
#30 /timable/limoncello-php/vendor/phpunit/phpunit/src/Framework/TestCase.php(883): PHPUnit\Framework\TestResult->run(Object(Tests\SubmissionApiTest))
#31 /timable/limoncello-php/vendor/phpunit/phpunit/src/Framework/TestSuite.php(744): PHPUnit\Framework\TestCase->run(Object(PHPUnit\Framework\TestResult))
#32 /timable/limoncello-php/vendor/phpunit/phpunit/src/TextUI/TestRunner.php(537): PHPUnit\Framework\TestSuite->run(Object(PHPUnit\Framework\TestResult))
#33 /timable/limoncello-php/vendor/phpunit/phpunit/src/TextUI/Command.php(212): PHPUnit\TextUI\TestRunner->doRun(Object(PHPUnit\Framework\TestSuite), Array, true)
#34 /timable/limoncello-php/vendor/phpunit/phpunit/src/TextUI/Command.php(141): PHPUnit\TextUI\Command->run(Array, true)
#35 /timable/limoncello-php/vendor/phpunit/phpunit/phpunit(53): PHPUnit\TextUI\Command::main()

@neomerx
Copy link
Collaborator

neomerx commented Aug 22, 2017

I think it should be an error in \Limoncello\Flute\Types\JsonApiDateTimeType::convertToDatabaseValue

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);
    }

@dreamsbond
Copy link
Author

yes. replaced with your snippet and the issue gone

@neomerx
Copy link
Collaborator

neomerx commented Aug 22, 2017

as for display order try to add isNumeric check

    public static function displayOrder(): RuleInterface
    {
        return self::isNumeric(self::stringToInt());
    }

@dreamsbond
Copy link
Author

dreamsbond commented Aug 22, 2017

i have two more concerns.
for nullable text validation
i used to do it like this:

    /**
     * @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
is there any guideline for proper validation?

@neomerx
Copy link
Collaborator

neomerx commented Aug 22, 2017

description looks fine.

If you don't use required then 2 options are fine: valid input and no input. If you use required the value has to be provided and it should be valid. So if no required used some default value in database should be used.

Built-in hasMany expect all values to have the same type. It would be complicated to support many types (and corresponding validators for ids) though possible.

hasMany usage should be simple. If you have any issues with it don't hesitate to ask.

@dreamsbond
Copy link
Author

    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
but in case of another lesson name which do not exists in the database, say "Geo Science"

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 📦

@neomerx
Copy link
Collaborator

neomerx commented Aug 22, 2017

You've found another bug 🎉

In \Limoncello\Validation\Rules\Comparisons\StringLengthBetween::__construct line 39

        parent::__construct($min, $min, ErrorCodes::STRING_LENGTH_BETWEEN, $errorContext);

should be replaced with

        parent::__construct($min, $max, ErrorCodes::STRING_LENGTH_BETWEEN, $errorContext);

@neomerx
Copy link
Collaborator

neomerx commented Aug 22, 2017

all fixes will be published later this week

@dreamsbond
Copy link
Author

dreamsbond commented Aug 22, 2017

i have originally a scenario on validator:

  • validate field with database for duplicate entry (check uniqueness) on CREATE; e.g.: name
  • validate field with database for duplicate entry (check uniqueness) on UPDATE, excluding the record itself which is editing; e.g.: name

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.

@neomerx
Copy link
Collaborator

neomerx commented Aug 22, 2017

so for Create it works fine for you now. No probs here.

As for Update I can see a few possible ways to solve the problem.

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)
b) Put to app container index of the resource and add custom Validation rule that uses that value (it has an access to app container)
c) Make custom validator which actually consists of 2 validators. The first one parses the input json and the second uses capture from the first one to set up rules for the second validator. This one is the most elegant and I'm working on similar issue currently. Though it's not finished yet so it's not an option at the moment.

Currently, the easiest one would be option b).

  • Add to container something like CurrentIndexStorage class with methods get/set index.
  • Override Controller::update and put index to CurrentIndexStorage
  • Add custom rule that takes Connection and CurrentIndexStorage from container and checks the value in the database.

@dreamsbond
Copy link
Author

i tried to do it in a custom validator like 'isUpdateUniqueRule'
but finally found no where to capture the index :(.. sad

ya, i think option b would be easier for me to do

@neomerx
Copy link
Collaborator

neomerx commented Aug 22, 2017

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.

@dreamsbond
Copy link
Author

the procedure would be:

  • create an configurator inside container folder
  • put get / set method
  • make it accessible in controller update
  • and use it with custom validator

am i correct?
btw, what is the convention of putting my own custom class?

@dreamsbond
Copy link
Author

i mean , is there any convention on where to put my own custom class

@neomerx
Copy link
Collaborator

neomerx commented Aug 22, 2017

there are no special requirements for placing custom rules. You can put it near to custom rule example \App\Json\Validators\Rules\IsEmailRule.

@dreamsbond
Copy link
Author

the option b works; though it was not really perfect at the moment. but i can really feel the power of container wise capability!!!

@neomerx
Copy link
Collaborator

neomerx commented Aug 27, 2017

@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.
Here is a new layout/structure.
The key change to make it work in sub-folders is this line in settings that instructs to search in sub folders (note **).

Some new features in Validation: new rule enum, new rule filter (a very very powerful tool that can check emails, URLs, sanitize input, and etc). The filter rule is a wrapper over PHP filter_var and has identical settings. Also, chaining capability was added to all applicable rules.

As for custom validation (option c)) I've got some good news as well. In Controller you can replace default Validator with a custom one which will add extra validation and will be able to use captured data.

    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 neomerx added the fixed label Aug 27, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants