alexanevsky / input-manager-bundle
提供函数,允许将传入的数据(例如,来自JSON)映射到对象中,修改它,验证它,并将其映射到模型或Doctrine实体
Requires
- php: >=8.1
- alexanevsky/getter-setter-accessor-bundle: ^1.0
- doctrine/orm: ^2.6
- symfony/config: ^5.4|^6.0
- symfony/dependency-injection: ^5.4|^6.0
- symfony/serializer: ^5.4|^6.0
- symfony/string: ^5.4|^6.0
- symfony/translation: ^5.4|^6.0
- symfony/validator: ^5.4|^6.0
This package is auto-updated.
Last update: 2024-10-01 00:07:36 UTC
README
这个库允许您将传入的数据(例如,来自JSON)映射到对象中,修改它,验证它,并将其映射到您的模型或Doctrine实体。这允许您将数据作为对象处理,同时防止模型或Doctrine实体无效的属性值。
该库由三个组件组成
- 反序列化器
- 验证器
- 输入数据到模型的映射器
让我们逐步分析每个组件。
目录
第一步
将 InputManager
添加到控制器或服务的构造函数中
use Alexanevsky\InputManagerBundle\InputManager; public function __construct( private InputManager $inputManager ) {}
反序列化器
基本示例
让我们想象我们有一些模型
class User { private string $firstName; private string $lastName; public function setFirstName(string $firstName): void { $this->firstName = $firstName; } public function setLastName(string $lastName): void { $this->lastName = $lastName; } }
要将请求数据映射到这个模型,我们将使用一个实现了 InputInterface
的中间对象,这样我们就可以在需要时检查和修改传入的数据
use Alexanevsky\InputManagerBundle\Input\InputInterface; class UserInput implements InputInterface { public string $firstName; public string $lastName; }
我们可以将属性描述为公开的。我们也可以使它们为私有的,并使用设置器和获取器
use Alexanevsky\InputManagerBundle\Input\InputInterface; class UserInput implements InputInterface { private string $firstName; private string $lastName; public function getFirstName(): string { return $this->firstName; } public function setFirstName(string $firstName): void { $this->firstName = $firstName; } public function getLastName(): string { return $this->lastName; } public function setLastName(string $lastName): void { $this->lastName = $lastName; } }
您可以使用任何您想要的方法。在本文档中,我们将使用公开属性。
注意:如果公开属性具有获取器(或设置器),则它将具有优先级,即获取器的值将被使用(传递给设置器),而不是从属性中获取(而不是分配给属性)。您可以在 alexanevsky/getter-setter-accessor 上了解更多有关获取器和设置器在库中如何使用的相关信息。
因此,我们需要做的第一步是将我们的请求反序列化为输入对象。
$json = '{"firstName": "John", "last_name": "Doe"}'; $input = new UserInput(); $this->deserializeInput($json, $input);
或者我们可以在反序列化过程中创建一个对象(两种方法都可以使用)
$json = '{"firstName": "John", "last_name": "Doe"}'; $input = $this->deserializeInput($json, UserInput::class);
请注意,反序列化器可以同时处理camelCase和snake_case键
因此,我们的 $input
将如下所示
echo $input->firstName; // John echo $input->lastName; // Doe
类型转换
反序列化器可以转换简单数据类型。
假设我们的输入期望以下数据
class UserInput implements InputInterface { public bool $anyBoolValue; public bool $anyAnotherBoolValue; public int $anyIntValue; }
我们将传递稍微错误类型的数据
$json = '{"any_bool_value": 1, "any_another_bool_value": '', "anyIntValue": "123"}'; $input = $this->deserializeInput($json, UserInput::class); echo $input->anyBoolValue; // true (bool) echo $input->anyAnotherBoolValue; // false (bool) echo $input->anyIntValue; // 123 (int)
正如我们所看到的,反序列化器成功地处理了这个任务,并将类型转换为所需的类型。
对象反序列化
假设我们有一个模型,它接受某些对象作为其属性。
class Address { private string $city; private string $street; private string $building; // Setters and getters of properties... } class User { private Address $address; // Setters and getters of properties... }
在我们的输入类中,我们必须使用它
class UserInput implements InputInterface { private Address $address; }
因此,我们的输入将从此JSON中成功反序列化
$json = '{"address": {"city": "string", "street": "string", "building": "string" }}';
嵌套输入反序列化
您的输入可以接受嵌套的输入,这些输入也将被反序列化,并最终转换为适当的模型对象。
假设我们有一个模型,它接受另一个模型作为其属性。
class Category { private string $name; // Getters and setters of properties... } class Article { private string $title; private Category $category; // Getters and setters of properties... }
这些模型将对应于以下输入类
class CategoryInput implements InputInterface { public string $name; } class ArticleInput implements InputInterface { public string $title; public CategoryInput $category; }
因此,我们的 ArticleInput
将成功地从这个JSON中反序列化
$json = '{"title": "Lorem Ipsum", "category": {"name": "Dolor"}}';
输入集合反序列化
如果我们的模型有一个属性,它不包含其他模型,而是包含模型数组,我们可以创建一个实现了InputCollectionInterface
(甚至更简单 - 扩展AbstractInputCollection
)的集合输入类,并定义其中需要使用的输入
use Alexanevsky\InputManagerBundle\Input\AbstractInputCollection; use Alexanevsky\InputManagerBundle\Input\InputCollectionInterface; class CategoryInput implements InputInterface { public string $name; } class CategoryInputCollection extends AbstractInputCollection { public function getClass(): string { return CategoryInput::class; } } class ArticleInput implements InputInterface { public string $title; public CategoryInputCollection $categories; }
因此,我们的 ArticleInput
将成功地从这个JSON中反序列化
$json = '{"title": "Lorem Ipsum", "categories": [{"name": "Dolor"}, {"name": "Sit"}]}';
通过标识符反序列化实体
想象一下,在请求数据中我们只有某个实体的标识符,但我们需要将其反序列化为实体本身
我们的实体看起来像这样
class Category { private string $id; // Getters and setters of properties... } class Article { private Category $category; // Getters and setters of properties... }
要获取通过id
传递的Category
对象,请将EntityFromId
属性添加到我们的输入类中,指定我们期望作为第一个参数的类
use Alexanevsky\InputManagerBundle\Input\Attribute\EntityFromId; class ArticleInput implements InputInterface { #[EntityFromId(Category::class)] public Category $category; }
因此,我们的 ArticleInput
将成功地从这个JSON中反序列化
$json = '{"category_id": 1}';
我们也可以不使用后缀
$json = '{"category": 1}';
如果我们的Category
的标识属性不同于id
,我们需要将标识属性的名字作为第二个参数传递给EntityFromId
class Category { private string $code; // Getters and setters of properties... } class Article { private Category $category; // Getters and setters of properties... } class ArticleInput implements InputInterface { #[EntityFromId(Category::class, 'code')] public Category $category; }
因此,我们的 ArticleInput
将成功地从这个JSON中反序列化
$json = '{"category_code": "cat"}';
我们也可以不使用后缀
$json = '{"category": "cat"}';
我们还可以通过第三个参数EntityFromId
指定我们期望在输入数据中的后缀
class ArticleInput implements InputInterface { #[EntityFromId(Category::class, 'code', 'identifier')] public Category $category; }
因此,我们的 ArticleInput
将成功地从这个JSON中反序列化
$json = '{"category_identifier": "cat"}';
我们也可以避免后缀,就像上面两个例子一样。
如果我们完全不想使用后缀,我们必须将false
作为EntityFromId
的第三个参数传递
class ArticleInput implements InputInterface { #[EntityFromId(Category::class, 'code', false)] public Category $category; }
通过标识符反序列化实体数组
上述描述的EntityFromId
属性也可以用于模型。
class Category { private string $id; // Getters and setters of properties... } class Article { private Collection $categories; // Getters and setters of properties... } class ArticleInput implements InputInterface { #[EntityFromId(Category::class)] public array $categories; }
有一点不同是,如果第二个参数(后缀)没有指定,反序列化器将期望它在末尾有s
。也就是说,我们的ArticleInput
可以从这个JSON成功反序列化
$json = '{"categories_ids": [1, 2]}';
我们也可以不使用后缀
$json = '{"categories": [1, 2]}';
输入修改器
我们可以通过实现InputModifiableInterface
(而不是InputInterface
)来创建我们的输入。这将允许我们添加一个modify()
方法,该方法将在反序列化后立即被调用。这将允许我们更改一些输入数据。
use Alexanevsky\InputManagerBundle\Input\InputModifiableInterface; class ArticleInput implements InputModifiableInterface { public string $title; public \DateTime $createdAt; public function modify(): void { $this->title .= ' from ' . $createdAt->format('m/d/Y'); } }
所以,给定以下请求
$json = '{"title": "Lorem Ipsum", "createdAt": "2023-01-01 12:00:00"}';
反序列化后,我们的输入将包含以下数据
echo $input->title; // 'Lorem Ipsum from 01/01/2023'
验证器
约束验证器
验证的最简单方法是使用Symfony Constraints。
让我们在我们的输入类上设置约束属性
use Symfony\Component\Validator\Constraints as Assert; class UserInput implements InputInterface { #[Assert\NotBlank] public string $name; #[Assert\NotBlank] #[Assert\Email] public string $email; }
在我们反序列化我们的输入后,让我们对其进行验证
use Symfony\Component\Translation\TranslatableMessage; /** @var TranslatableMessage[] $errors */ $errors = $this->inputManager->validate($input);
结果,我们将得到一个错误关联数组,其中键是发生错误的属性名,值是带有错误信息的TranslatableMessage
。
扩展验证器
我们可以创建自己的扩展验证器,该验证器实现了InputValidatorInterface
(甚至更简单 - 扩展AbstractInputValidator
)。在它里面,我们指定validate()
方法,该方法将执行必要的检查并返回一个错误关联数组,其中键是发生错误的属性名,值是带有错误信息的TranslatableMessage
。如果它返回一个空数组,则表示验证成功。
use App\Component\InputManager\InputValidator\AbstractInputValidator; class UserInput implements InputInterface { public string $name; public string $email; } class UserInputValidator extends AbstractInputValidator { public function validate(): array { $errors = []; if (!$this->getInput()->name) { $errors['name'] = new TranslatableMessage('Name is empty!'); } if (!$this->getInput()->email) { $errors['email'] = new TranslatableMessage('Email is empty!'); } elseif (filter_var($this->getInput()->email, FILTER_VALIDATE_EMAIL)) { $errors['email'] = new TranslatableMessage('Email is incorrect!'); } return $errors = []; } }
要使用此验证器,指定其类名作为验证方法的第二个参数
$errors = $this->inputManager->validate($input, UserInputValidator::class);
请注意,扩展验证器只有在没有定义约束错误的情况下才会被调用。
我们可以选择另一种方式:在我们的扩展验证器中,通过在方法名前添加validate
到属性名来为每个属性创建验证方法。如果有错误,我们必须返回TranslatableMessage
,如果没有错误,则返回null。
class UserInputValidator extends AbstractInputValidator { public function validateName(): ?TranslatableMessage { if (!$this->getInput()->name) { return new TranslatableMessage('Name is empty!'); } return null; } public function validateEmail(): ?TranslatableMessage { if (!$this->getInput()->email) { return new TranslatableMessage('Email is empty!'); } elseif (filter_var($this->getInput()->email, FILTER_VALIDATE_EMAIL)) { return new TranslatableMessage('Email is incorrect!'); } return null; } }
扩展验证器有效载荷
我们还可以将一些有效负载传递给我们的验证器以用于验证。为此,我们将添加一个公共属性并添加SetFromPayload
属性,这将告诉验证器属性的值应从有效负载中接收。我们还可以设置布尔参数required
。如果required
为true
并且没有数据在有效负载中,我们将引发异常。例如,我们可以将当前正在处理的用户的实体传递给验证器。
use Alexanevsky\InputManagerBundle\InputValidator\Attribute\SetFromPayload; class UserInputValidator extends AbstractInputValidator { #[SetFromPayload(true)] public User $user; public function __construct( private UserRepository $usersRepository ) { } public function validateEmail(): ?TranslatableMessage { $foundedUser = $this->usersRepository->findOneBy(['email' => $this->getInput()->email]); return !$foundedUser || $this->user === $foundedUser ? null : new TranslatableMessage('User with this email already exists!'); } }
要将有效负载传递给扩展验证器,将其作为验证方法的第三个参数传递
$errors = $this->inputManager->validate($input, UserInputValidator::class, ['user' => $user]);
输入到对象(模型)映射器
最后,在完成反序列化和验证后,我们需要将数据从我们的输入映射到我们的模型。我们将通过简单的方法来完成这项工作
$this->$inputManager->mapInputToObject($input, $user);
我们将所有从输入反序列化有效的数据通过公共属性和设置器设置给用户。
祝你好运!