Proof of concept смешанной гидрации данных для Eloquent
Работая с данными по API, мы часто получаем эти сущности с вложенными отношениями, но вот при отправке данных, вложенные отношения приходится обрабатывать вручную. Данный гидратор позволяет не думать об этом. И это сильно ускоряет разработку.
- Позволяет работать с вводом и выводом моделей Eloquent "как есть" - без лишних манипуляций
- Unit of work - или будут выполнены все изменения, или (в случае ошибки) изменения не будут выполнены вообще
- Idenity map - гарантирует, что сущности одного типа и с одинаковым идетификатором - есть суть одно
- uuid - позволяет создавать валидные сущности и связывать их между собой по идентификатору, не обращаясь к базе данных
- На данный момент работает только с uuid
composer require greabock/populator
Использовать эту штуку очень просто
<?php
namespace App\Http\Controllers;
use App\Post;
use Exception;
use Greabock\Populator\Populator;
use Illuminate\Http\Request;
class PostController
{
/**
* @param $id
* @param Request $request
* @param Populator $populator
* @return Post
* @throws Exception
*/
public function put(Request $request, Populator $populator): Post
{
$post = Post::findOrNew($request->get('id'));
$populator->populate($post, $request->all());
// здесь мы можем сделать что-то до того, как изменения отправятся в базу.
$populator->flush();
return $post;
}
}
(не делайте так - это просто пример)
import uuid from 'uuid/v4'
class Post {
constructor(data) {
if(!data.id) {
data.id = uuid()
}
Object.assign(this, data)
}
addTag (tag) {
this.tags.push(tag)
}
addImage (image) {
this.images.push(image)
}
}
class Tag {
constructor(data) {
if(!data.id) {
data.id = uuid()
}
Object.assign(this, data)
}
}
let post, tags;
//
function loadTags () {
fetch('tags')
.then(response => response.json())
.then(tagsData => tags = data.map(tagdata => new Tag(tagdata)))
}
function loadPost (id) {
fetch(`posts/${id}`)
.then(response => response.json())
.then(data => post = new Post(data))
}
function savePost(post) {
fetch(`posts/${post.id}`, {method: 'PUT', body: JSON.stringify(post)})
.then(response => response.json())
.then(data => alert(`Post ${data.title} saved!`))
}
loadTags()
loadPost(1)
// После того, как всё загружено:
post.addTag(tags[0])
post.title = 'Hello World!'
savePost(post)
Возьмем простой пример:
{
"name": "Greabock",
"email": "greabock@gmail.com",
}
Так как в переданных данных отсутствует поле id
(или другое поле, которе было укзано в $primaryKey
модели), гидратор создаст новую сущность. И наполнит ее передаными данными используя стандартный метод fill
.
В этом случае для модели будет сразу же сгенерирован id
.
Пример с идентификатором:
{
"id" : "123e4567-e89b-12d3-a456-426655440000",
"name": "Greabock",
"email": "greabock@gmail.com",
}
В этом примере id
был передан - поэтому гидратор попытается найти такую сущность в базе данных. Однако, если у него не получится найти такую запись в базе данных, то он создаст новую сущность с переданным id
.
В любом случае, гидратор заполнит эту модель переданными email
и name
. В этом случае, поведение похоже на User::findORNew($id)
.
{
"id": "123e4567-e89b-12d3-a456-426655440000",
"name": "Greabock",
"email": "greabock@gmail.com",
"account": {
"active": true,
}
}
В данном случае, гидратор поступит с сущностью перового уровня (пользователем) так же, как в примере с идентификатором. Затем, он попытается найти и аккаунт - если не найдет (а в текущем примере у аккаунта нет id
), то создаст новый. Если найдет но с другим идентификатором, то заместит его вновь созданным. Старый же аккаунт будет удалён. Само собой в всязанное поле поста (например user_id
или author_id
- в зависимости от того, как это указано в отношении User::account()
), будет записан идентификатор пользователя.
{
"id": "123e4567-e89b-12d3-a456-426655440000",
"name": "Greabock",
"email": "greabock@gmail.com",
"posts": [
{
"id": "1286d5bb-c566-4f3e-abe0-4a5d56095f01",
"title": "foo",
"text": "bar"
},
{
"id": "d91c9e65-3ce3-4bea-a478-ee33c24a4628",
"title": "baz",
"text": "quux"
},
{
"title": "baz",
"text": "quux"
}
]
}
В примере с отношением, "многие к одному", гидратор поступит с каждой записью поста, как в примерере HasOne
. Кроме того, все записи, которые не были представлены в переданном массиве постов, будут удалены.
{
"id" : "123e4567-e89b-12d3-a456-426655440000",
"name": "Greabock",
"email": "greabock@gmail.com",
"organization": {
"id": "1286d5bb-c566-4f3e-abe0-4a5d56095f01",
"name": "Acme"
},
}
Хотя этот пример и выглядит как HasOne
, работает он иначе. Если такая организация будет найдена гидратором в базе данных, то пользователь будет к ней привязан через поле отношения. С другой стороны, если такой записи не будет, то пользователь получит в это поле null
. Все прочие поля связанной записи (организации) будут проигнорированы - так как User
не является aggregate root
по отношению к Organization
, следовательно, нельзя управлять полями организации через объект пользователя, как нельзя и создать новые организации.
{
"id" : "123e4567-e89b-12d3-a456-426655440000",
"name": "Greabock",
"email": "greabock@gmail.com",
"roles": [
{
"id": "dcb41b0c-8bc1-490c-b714-71a935be5e2c",
"pivot": { "sort": 0 }
}
]
}
Этот пример похож на смесь из HasMany
(в том смысле, что все непредставленные записи будут удалены из пивота) и BlongsTo
(все поля, кроме поля $primaryKey
будут проигнорированы, по причинам изложеным выше в разделе belongsTo
). Обратите внимание, что работа с пивотом так же доступна.
Всё описанное работает рекурсивно, и справедливо для любой степени вложенности.
Стоит также отметить, что все переданные отношения будут добавлены в сущности при выводе. Например:
$user = $populator->populate(User::class, [
'id' => '123e4567-e89b-12d3-a456-426655440000',
'name' => 'Greabock',
'email' => 'greabock@gmail.com',
'roles' => [
[
'id' => 'dcb41b0c-8bc1-490c-b714-71a935be5e2c',
'pivot' => ['sort' => 0],
],
],
]);
$user->relationLoaded('roles'); // true
// хотя flush еще не сделан, все отношения уже прописаны, и нет необходимости загружать их дополнтительно.
// Обращение к $user->roles - не вызовет повтороно запроса к бд.
$populator->flush();
// Только после этого сущность со всеми ее связями попадёт в базу данных.
- добавить возможность персиста сущности не прошедшей через гидратор
- добавить в readme описание полиморфных отношений