Skip to content

greabock/populator

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

76 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Populator

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": "[email protected]",
}

Так как в переданных данных отсутствует поле id (или другое поле, которе было укзано в $primaryKey модели), гидратор создаст новую сущность. И наполнит ее передаными данными используя стандартный метод fill. В этом случае для модели будет сразу же сгенерирован id.

Пример с идентификатором:

{
  "id" : "123e4567-e89b-12d3-a456-426655440000",
  "name": "Greabock",
  "email": "[email protected]",
}

В этом примере id был передан - поэтому гидратор попытается найти такую сущность в базе данных. Однако, если у него не получится найти такую запись в базе данных, то он создаст новую сущность с переданным id . В любом случае, гидратор заполнит эту модель переданными email и name. В этом случае, поведение похоже на User::findORNew($id).

🔝

HasOne

{
  "id": "123e4567-e89b-12d3-a456-426655440000",
  "name": "Greabock",
  "email": "[email protected]",
  "account": {
    "active": true,
  }
}

В данном случае, гидратор поступит с сущностью перового уровня (пользователем) так же, как в примере с идентификатором. Затем, он попытается найти и аккаунт - если не найдет (а в текущем примере у аккаунта нет id), то создаст новый. Если найдет но с другим идентификатором, то заместит его вновь созданным. Старый же аккаунт будет удалён. Само собой в всязанное поле поста (например user_id или author_id - в зависимости от того, как это указано в отношении User::account()), будет записан идентификатор пользователя.

🔝

HasMany

{
  "id": "123e4567-e89b-12d3-a456-426655440000",
  "name": "Greabock",
  "email": "[email protected]",
  "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. Кроме того, все записи, которые не были представлены в переданном массиве постов, будут удалены.

🔝

BelongsTo

{
  "id" : "123e4567-e89b-12d3-a456-426655440000",
  "name": "Greabock",
  "email": "[email protected]",
  "organization": {
    "id": "1286d5bb-c566-4f3e-abe0-4a5d56095f01",
    "name": "Acme"
  },
}

Хотя этот пример и выглядит как HasOne, работает он иначе. Если такая организация будет найдена гидратором в базе данных, то пользователь будет к ней привязан через поле отношения. С другой стороны, если такой записи не будет, то пользователь получит в это поле null. Все прочие поля связанной записи (организации) будут проигнорированы - так как User не является aggregate root по отношению к Organization, следовательно, нельзя управлять полями организации через объект пользователя, как нельзя и создать новые организации.

🔝

BelongsToMany

{
  "id" : "123e4567-e89b-12d3-a456-426655440000",
  "name": "Greabock",
  "email": "[email protected]",
  "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' => '[email protected]',
        'roles' => [
            [
              'id'    => 'dcb41b0c-8bc1-490c-b714-71a935be5e2c',
              'pivot' => ['sort' => 0],
            ],
        ],
    ]);
    
    $user->relationLoaded('roles'); // true
    // хотя flush еще не сделан, все отношения уже прописаны, и нет необходимости загружать их дополнтительно.
    // Обращение к $user->roles - не вызовет повтороно запроса к бд.

    $populator->flush();
    // Только после этого сущность со всеми ее связями попадёт в базу данных. 

🔝

TODO

  • добавить возможность персиста сущности не прошедшей через гидратор
  • добавить в readme описание полиморфных отношений

🔝

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages