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

doc: Review and test validation cookbook #2662

Merged
merged 4 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 ``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. 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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that this cookbook was copied from a blog post.

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,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing to throw an error. Reading the server logs is totally disconnected from the application code, a developer will prefer having an exception with the context and the stacktrace.

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'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to use a public constant when it's read by the attribute on the class, that was required for the doctrine annotation.

{
"$jsonSchema": {
"required": ["name"],
Expand Down
1 change: 1 addition & 0 deletions lib/Doctrine/ODM/MongoDB/DocumentManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,7 @@ public function getRepository($className)
* @psalm-param CommitOptions $options
*
* @throws MongoDBException
* @throws Throwable From event listeners.
*/
public function flush(array $options = [])
{
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';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only updated comments to have a better tooltip in the IDE. The constant definition haven't been modified.


/**
* 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
Loading