Skip to content

Commit

Permalink
Review and test validation cookbook
Browse files Browse the repository at this point in the history
  • Loading branch information
GromNaN committed Jun 28, 2024
1 parent e40c8b1 commit 202895c
Show file tree
Hide file tree
Showing 9 changed files with 316 additions and 48 deletions.
80 changes: 40 additions & 40 deletions docs/en/cookbook/validation-of-documents.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ is allowed to:
<?php
#[Document]
class Order
{
public function assertCustomerAllowedBuying(): void
Expand Down Expand Up @@ -68,8 +69,7 @@ First Attributes:
#[HasLifecycleCallbacks]
class Order
{
#[PrePersist]
#[PreUpdate]
#[PreFlush]
public function assertCustomerAllowedBuying(): void {}
}
Expand All @@ -78,17 +78,21 @@ First Attributes:
<doctrine-mapping>
<document name="Order">
<lifecycle-callbacks>
<lifecycle-callback type="prePersist" method="assertCustomerallowedBuying" />
<lifecycle-callback type="preUpdate" method="assertCustomerallowedBuying" />
<lifecycle-callback type="preFlush" method="assertCustomerallowedBuying" />
</lifecycle-callbacks>
</document>
</doctrine-mapping>
Now validation is performed whenever you call
``DocumentManager#persist($order)`` or when you call
``DocumentManager#flush()`` and an order is about to be updated. Any
Exception that happens in the lifecycle callbacks will be cached by
the DocumentManager.
Now validation is performed when you call ``DocumentManager#flush()`` and an
order is about to be inserted or updated. Any Exception that happens in the
lifecycle callbacks will stop the flush operation and the exception will be
propagated.

You might want to use the ``PrePersist`` instead of ``PreFlush`` to validate
the document sooner, when you call ``DocumentManager#persist()``. This way you
can catch validation errors earlier in your application flow. But be aware that
if the document is modified after the ``PrePersist`` event, the validation
might not be triggered again and an invalid document can be persisted.

Of course you can do any type of primitive checks, not null,
email-validation, string size, integer and date ranges in your
Expand All @@ -102,8 +106,7 @@ validation callbacks.
#[HasLifecycleCallbacks]
class Order
{
#[PrePersist]
#[PreUpdate]
#[PreFlush]
public function validate(): void
{
if (!($this->plannedShipDate instanceof DateTime)) {
Expand All @@ -128,11 +131,8 @@ can register multiple methods for validation in "PrePersist" or
"PreUpdate" or mix and share them in any combinations between those
two events.

There is no limit to what you can and can't validate in
"PrePersist" and "PreUpdate" as long as you don't create new document
instances. This was already discussed in the previous blog post on
the Versionable extension, which requires another type of event
called "onFlush".
There is no limit to what you can validate in ``PreFlush``, ``PrePersist`` and
``PreUpdate`` as long as you don't create new document instances.

Further readings: :doc:`Lifecycle Events <../reference/events>`

Expand Down Expand Up @@ -181,44 +181,44 @@ the ``odm:schema:create`` or ``odm:schema:update`` command.
#[ODM\Document]
#[ODM\Validation(
validator: self::VALIDATOR,
action: ClassMetadata::SCHEMA_VALIDATION_ACTION_WARN,
level: ClassMetadata::SCHEMA_VALIDATION_LEVEL_MODERATE,
action: ClassMetadata::SCHEMA_VALIDATION_ACTION_ERROR,
level: ClassMetadata::SCHEMA_VALIDATION_LEVEL_STRICT,
)]
class SchemaValidated
{
public const VALIDATOR = <<<'EOT'
{
"$jsonSchema": {
"required": ["name"],
"properties": {
"name": {
"bsonType": "string",
"description": "must be a string and is required"
}
private const VALIDATOR = <<<'EOT'
{
"$jsonSchema": {
"required": ["name"],
"properties": {
"name": {
"bsonType": "string",
"description": "must be a string and is required"
}
}
},
"$or": [
{ "phone": { "$type": "string" } },
{ "email": { "$regularExpression" : { "pattern": "@mongodb\\.com$", "options": "" } } },
{ "status": { "$in": [ "Unknown", "Incomplete" ] } }
]
}
},
"$or": [
{ "phone": { "$type": "string" } },
{ "email": { "$regularExpression" : { "pattern": "@mongodb\\.com$", "options": "" } } },
{ "status": { "$in": [ "Unknown", "Incomplete" ] } }
]
}
EOT;
EOT;
#[ODM\Id]
private $id;
public string $id;
#[ODM\Field(type: 'string')]
private $name;
public string $name;
#[ODM\Field(type: 'string')]
private $phone;
public string $phone;
#[ODM\Field(type: 'string')]
private $email;
public string $email;
#[ODM\Field(type: 'string')]
private $status;
public string $status;
}
.. code-block:: xml
Expand Down
2 changes: 1 addition & 1 deletion docs/en/reference/attributes-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1229,7 +1229,7 @@ for the related collection.
)]
class SchemaValidated
{
public const VALIDATOR = <<<'EOT'
private const VALIDATOR = <<<'EOT'
{
"$jsonSchema": {
"required": ["name"],
Expand Down
45 changes: 38 additions & 7 deletions lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -325,20 +325,51 @@
public const REFERENCE_STORE_AS_REF = 'ref';

/**
* The collection schema validationAction values
* Rejects any insert or update that violates the validation criteria.
*
* @see https://docs.mongodb.com/manual/core/schema-validation/#accept-or-reject-invalid-documents
* Value for collection schema validationAction.
*
* @see https://www.mongodb.com/docs/manual/core/schema-validation/handle-invalid-documents/#option-1--reject-invalid-documents
*/
public const SCHEMA_VALIDATION_ACTION_ERROR = 'error';
public const SCHEMA_VALIDATION_ACTION_WARN = 'warn';

/**
* The collection schema validationLevel values
* MongoDB allows the operation to proceed, but records the violation in the MongoDB log.
*
* Value for collection schema validationAction.
*
* @see https://www.mongodb.com/docs/manual/core/schema-validation/handle-invalid-documents/#option-2--allow-invalid-documents--but-record-them-in-the-log
*/
public const SCHEMA_VALIDATION_ACTION_WARN = 'warn';

/**
* Disable schema validation for the collection.
*
* Value of validationLevel.
*
* @see https://www.mongodb.com/docs/manual/core/schema-validation/specify-validation-level/
*/
public const SCHEMA_VALIDATION_LEVEL_OFF = 'off';

/**
* MongoDB applies the same validation rules to all document inserts and updates.
*
* Value of validationLevel.
*
* @see https://www.mongodb.com/docs/manual/core/schema-validation/specify-validation-level/#steps--use-strict-validation
*/
public const SCHEMA_VALIDATION_LEVEL_STRICT = 'strict';

/**
* MongoDB applies the same validation rules to document inserts and updates
* to existing valid documents that match the validation rules. Updates to
* existing documents in the collection that don't match the validation rules
* aren't checked for validity.
*
* Value of validationLevel.
*
* @see https://docs.mongodb.com/manual/core/schema-validation/#existing-documents
* @see https://www.mongodb.com/docs/manual/core/schema-validation/specify-validation-level/#steps--use-moderate-validation
*/
public const SCHEMA_VALIDATION_LEVEL_OFF = 'off';
public const SCHEMA_VALIDATION_LEVEL_STRICT = 'strict';
public const SCHEMA_VALIDATION_LEVEL_MODERATE = 'moderate';

/* The inheritance mapping types */
Expand Down
22 changes: 22 additions & 0 deletions tests/Documentation/Validation/Customer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Documentation\Validation;

use Doctrine\ODM\MongoDB\Mapping\Annotations\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations\Field;
use Doctrine\ODM\MongoDB\Mapping\Annotations\Id;

#[Document]
class Customer
{
#[Id]
public string $id;

public function __construct(
#[Field(type: 'float')]
public float $orderLimit,
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Documentation\Validation;

use RuntimeException;

class CustomerOrderLimitExceededException extends RuntimeException
{
}
47 changes: 47 additions & 0 deletions tests/Documentation/Validation/Order.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Documentation\Validation;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ODM\MongoDB\Mapping\Annotations\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations\EmbedMany;
use Doctrine\ODM\MongoDB\Mapping\Annotations\HasLifecycleCallbacks;
use Doctrine\ODM\MongoDB\Mapping\Annotations\Id;
use Doctrine\ODM\MongoDB\Mapping\Annotations\PreFlush;
use Doctrine\ODM\MongoDB\Mapping\Annotations\ReferenceOne;

#[Document]
#[HasLifecycleCallbacks]
class Order
{
#[Id]
public string $id;

public function __construct(
#[ReferenceOne(targetDocument: Customer::class)]
public Customer $customer,
/** @var Collection<OrderLine> */
#[EmbedMany(targetDocument: OrderLine::class)]
public Collection $orderLines = new ArrayCollection(),
) {
}

/** @throw CustomerOrderLimitExceededException */
#[PreFlush]
public function assertCustomerAllowedBuying(): void
{
$orderLimit = $this->customer->orderLimit;

$amount = 0;
foreach ($this->orderLines as $line) {
$amount += $line->amount;
}

if ($amount > $orderLimit) {
throw new CustomerOrderLimitExceededException();
}
}
}
22 changes: 22 additions & 0 deletions tests/Documentation/Validation/OrderLine.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Documentation\Validation;

use Doctrine\ODM\MongoDB\Mapping\Annotations\EmbeddedDocument;
use Doctrine\ODM\MongoDB\Mapping\Annotations\Field;
use Doctrine\ODM\MongoDB\Mapping\Annotations\Id;

#[EmbeddedDocument]
class OrderLine
{
#[Id]
public string $id;

public function __construct(
#[Field(type: 'float')]
public float $amount,
) {
}
}
51 changes: 51 additions & 0 deletions tests/Documentation/Validation/SchemaValidated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace Documentation\Validation;

use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;

#[ODM\Document]
#[ODM\Validation(
validator: self::VALIDATOR,
action: ClassMetadata::SCHEMA_VALIDATION_ACTION_ERROR,
level: ClassMetadata::SCHEMA_VALIDATION_LEVEL_STRICT,
)]
class SchemaValidated
{
private const VALIDATOR = <<<'EOT'
{
"$jsonSchema": {
"required": ["name"],
"properties": {
"name": {
"bsonType": "string",
"description": "must be a string and is required"
}
}
},
"$or": [
{ "phone": { "$type": "string" } },
{ "email": { "$regularExpression" : { "pattern": "@mongodb\\.com$", "options": "" } } },
{ "status": { "$in": [ "Unknown", "Incomplete" ] } }
]
}
EOT;

#[ODM\Id]
public string $id;

#[ODM\Field(type: 'string')]
public string $name;

#[ODM\Field(type: 'string')]
public string $phone;

#[ODM\Field(type: 'string')]
public string $email;

#[ODM\Field(type: 'string')]
public string $status;
}
Loading

0 comments on commit 202895c

Please sign in to comment.