SF6 / Sonata 5: Quản lý giao diện OneToMany trong một CRUD với CollectionType của Sonata

Trong ví dụ này, chúng ta sẽ xem xét trường hợp cụ thể của một giao diện đơn giản bao gồm nhiều yếu tố.
Chúng ta đã chọn sử dụng bảng "Dự án" trong đó chúng ta sẽ gán tài liệu.
Vì vậy, đối với một dự án, chúng ta có thể gán nhiều tài liệu. Và để thuận tiện trong quản lý, chúng ta sẽ đảm bảo rằng chúng ta có thể quản lý giao diện lồng nhau trực tiếp trong giao diện chỉnh sửa của dự án của mình.

Chúng ta cần tạo sơ đồ.
Đầu tiên, chúng ta nhập sơ đồ hiện tại của mình vào MysqlWorbench bằng cách sử dụng chức năng "Database/reverse engineer". Điều này sẽ giúp chúng ta có các bảng mặc định của Sonata vì chúng ta sẽ chơi với phương tiện truyền thông.



Với MysqlWorbench, chúng ta chọn mối quan hệ 1->N giữa bảng "dự án" của chúng ta và bảng "projects_medias" và sau đó là mối quan hệ N->1 giữa bảng "projects_medias" và bảng "media__media".

1->N = OneToMany
N->1 = ManyToOne

Sự tinh tế nằm ở việc đặt một id tự tăng mặc định trong bảng liên kết của chúng ta và không sử dụng khóa ngoại của chúng ta làm khóa chính.

Đây là kết quả chúng ta nhận được:

projet_id sẽ chứa id từ bảng dự ánmedia_id sẽ chứa id từ bảng media__media.
Chúng ta thêm một trường thứ tự nhưng chúng ta có thể thêm bao nhiêu trường tùy ý. Bởi vì chúng ta sẽ xem cách quản lý toàn bộ biểu mẫu để chỉnh sửa các yếu tố của projects_medias.

Trong MysqlWorbench, chúng ta chọn "Forward/reverse engineer" để đẩy sơ đồ của mình vào MySQL.

Chúng ta thực hiện một thủ thuật cuối cùng với các lệnh Symfony để tạo ra các thực thể và giao diện quản trị mặc định của chúng ta.

bin/console doctrine:mapping:import "App\Entity" annotation --path=src/Entity
bin/console make:entity --regenerate App
bin/console make:sonata:admin App/Entity/Projets
bin/console make:sonata:admin App/Entity/ProjetsMedias

Đó là điều kỳ diệu nhưng chưa hoàn chỉnh. Chúng ta cần sửa lại các mối quan hệ trong 2 thực thể của chúng ta.
Chúng ta đã định nghĩa các thuộc tính truy cập trong cả hai hướng.

Đối với thực thể App\Entity\Projets, đó sẽ là thuộc tính tài liệu, với các truy cập addDocumentremoveDocument


use Doctrine\Common\Collections\Collection;
...
...
private $documents;

public function getDocuments(): Collection
{

    return $this->documents;
}

public function addDocument(ProjetsMedias $document): self
{
    if (!$this->documents->contains($document)) {
        $this->documents[] = $document;
        $document->setProjets($this);
    }

    return $this;
}

public function removeDocument(ProjetsMedias $document): self
{
    if ($this->documents->removeElement($document)) {
        // set the owning side to null (unless already changed)
        if ($document->getProjets() === $this) {
            $document->setProjets(null);
        }
    }

    return $this;
}


Đối với thực thể App\Entity\ProjetsMedias, đó sẽ là thuộc tính dự án, với các truy cập getProjets()setProjets().

private $projets;

public function getProjets(): ?Projets
{
    return $this->projets;
}

public function setProjets(?Projets $projets): self
{
    $this->projets = $projets;
    return $this;
}

Hãy tập trung vào cấu hình mối quan hệ Projets->ProjetsMedias vì đây là nơi mọi thứ diễn ra.

/**
 * @var \Doctrine\Common\Collections\Collection
 *
 * @ORM\OneToMany(targetEntity="App\Entity\ProjetsMedias", mappedBy="projets", cascade={"persist", "remove" })
 * @ORM\JoinTable(name="projets_medias",
 *   joinColumns={
 *     @ORM\JoinColumn(name="projets_id", referencedColumnName="id")
 *   }
 * )
 */
private $documents;

Thuộc tính targetEntity của OneToMany của chúng ta phải chỉ đến thực thể của bảng chứa danh sách tài liệu và chúng ta phải cung cấp thuộc tính của mối quan hệ ManyToOne của thực thể này.
Vì vậy, đây là \App\Entity\ProjetsMedias có thuộc tính dự án. Sau đó chúng ta đặt tên cho bảng MySQL mà nó phải tham gia: projets_media. JoinColumn định nghĩa tên của trường trong bảng đã tham gia sẽ chứa khóa chính của dự án của chúng ta, vì vậy giá trị của thuộc tính id của chúng ta, phải được lưu trữ trong projets_id của projets_media.

Sau đó chúng ta cấu hình mối quan hệ của chúng ta trong ProjetsMedia.

/**
 * @var \Projets
 *
 * @ORM\ManyToOne(targetEntity="Projets",inversedBy="documents")
 * @ORM\JoinColumns({
 *   @ORM\JoinColumn(name="projets_id", referencedColumnName="id")
 * })
 */
private $projets;

public function getProjets(): ?Projets
{
    return $this->projets;
}

public function setProjets(?Projets $projets): self
{
    $this->projets = $projets;

    return $this;
}

Thuộc tính targetEntity của ManyToOne của chúng ta phải chỉ đến thực thể của bảng quản lý dự án và chúng ta phải cung cấp thuộc tính của mối quan hệ OneToMany của thực thể này.
Vì vậy, đây là Projets có thuộc tính tài liệu.
Sau đó chúng ta đặt tên cho trường của bảng hiện tại của chúng ta (ProjetsMedia) lưu trữ khóa chính của dự án. Vì vậy, id từ bảng dự án được lưu trữ trong projets_id của projets_media

Đây không gì khác hơn là ngược lại của những gì chúng ta đã cấu hình cho định nghĩa của \App\Entity\Projets::$tài liệu

Bước cuối cùng, vì chúng ta đã chọn chơi với phương tiện truyền thông, chúng ta phải cấu hình mối quan hệ của chúng ta với thư viện phương tiện truyền thông của Sonata và cấu hình các truy cập.

use Sonata\MediaBundle\Model\MediaInterface;
...
...
/**
 * @var \MediaMedia
 *
 * @ORM\ManyToOne(targetEntity="App\Application\Sonata\MediaBundle\Entity\Media")
 * @ORM\JoinColumns({
 *   @ORM\JoinColumn(name="media_id", referencedColumnName="id")
 * })
 */
private $mediaMedia;

public function __toString(): string
{
    return $this->getMediaMedia();
}

public function getMediaMedia(): ?MediaInterface
{

    return $this->mediaMedia;
}

public function setMediaMedia(?MediaInterface $mediaMedia): self
{
    $this->mediaMedia = $mediaMedia;

    return $this;
}

Giống như trên. Chúng ta bắt đầu bằng cách đưa ra thực thể mục tiêu, ở đây là "App\Application\Sonata\MediaBundle\Entity\Media". Bởi vì đó là thực thể mà chúng ta đã mở rộng trong cấu hình của chúng ta cho gói Sonata/Media.

Trong sonata_media.yml, chúng ta có điều này:

sonata_media:
   class:
        media: App\Application\Sonata\MediaBundle\Entity\Media
        gallery: App\Application\Sonata\MediaBundle\Entity\Gallery
        gallery_item: App\Application\Sonata\MediaBundle\Entity\GalleryItem
        category: App\Application\Sonata\ClassificationBundle\Entity\SonataClassificationCategory

Điều này có vẻ hiển nhiên nhưng nếu bạn đọc bài viết này mà không có tất cả kiến thức cơ bản, thì việc nhắc nhở bạn có vẻ hợp lý. Nó có thể giải quyết được nhiều vấn đề cho nhiều người. 😊

Sau đó JoinColumn định nghĩa cách chúng ta ghi lại mối quan hệ. Vì vậy, đó là trường media_id từ bảng projets_media của chúng ta sẽ chứa trường id của bảng quản lý phương tiện truyền thông (media__media).

Bây giờ chúng ta chỉ cần cấu hình trường phương tiện truyền thông của giao diện quản trị của chúng ta mà chúng ta đã tự động tạo ra phía trên (ProjetsMediasAdmin.php).
Để quản lý một trường phương tiện truyền thông, bạn chỉ cần sử dụng ModelListType của Sonata. Bạn tự do kích hoạt các nút thêm/sửa/xem/xóa tùy chọn.

use Sonata\AdminBundle\Form\Type\ModelListType;
...
...
protected function configureFormFields(FormMapper $form): void
{
    $form
        ->add('projets')
        ->add('mediaMedia', ModelListType::class, [
            'required' => false,
            'btn_add'=>true,
            'btn_edit'=>false,
            'btn_list'=>false,
            'btn_delete'=>false,
        ])
        ->add('ordre')
    ;
}

Các mối quan hệ bây giờ đã hoàn chỉnh. Chúng ta có thể kiểm tra xem mọi thứ có hoạt động bằng cách kích hoạt giao diện quản trị ProjetsMedia của chúng ta.

Trong dịch vụ của chúng ta, việc tạo ra các giao diện của chúng ta đã được cấu hình như sau:

    admin.projets:
        class: App\Admin\ProjetsAdmin
        tags:
            - { name: sonata.admin, model_class: App\Entity\Projets, controller: App\Controller\ProjetsAdminController, manager_type: orm, group: admin, label: Projets }

    admin.projets_medias:
        class: App\Admin\ProjetsMediasAdmin
        tags:
            - { name: sonata.admin, model_class: App\Entity\ProjetsMedias, controller: App\Controller\ProjetsMediasAdminController, manager_type: orm, group: admin, label: ProjetsMedias }
        arguments: [~, App\Entity\ProjetsMediasAdmin, ~]

Vì vậy, bây giờ chúng ta có thể truy cập trực tiếp vào giao diện của mình để xem nó có hoạt động không.

Nhưng điều chúng ta quan tâm là có giao diện quản lý này trực tiếp trong quản lý Dự án của chúng ta.

Và để làm điều đó, chúng ta sẽ sử dụng CollectionType của Sonata.
Chúng ta thêm vào giao diện quản trị tự động tạo của chúng ta, trong phương thức quản lý cấu hình biểu mẫu chỉnh sửa, một trường tài liệu sẽ có CollectionType là loại của nó với cấu hình của nó, và quan trọng nhất, chúng ta thêm tham số admin_code chỉ đến tên của dịch vụ quản trị projets_media của chúng ta.

$form->add('documents', \Sonata\Form\Type\CollectionType::class,
    [
        'required' => false,
        'by_reference' => false,
        'label'=> false],
    [
        'edit' => 'inline',
        'inline' => 'table',
        'sortable' => 'position',
        'link_parameters' => [
            'context' => 'default',
            'provider' => 'sonata.media.provider.file',
            'hide_context' => true
        ],
        'admin_code' => 'admin.projets_medias'
    ]
);

Nhân tiện, trong \App\Admin\ProjetsMediasAdmin, chúng ta loại bỏ trường dự án của chúng ta vì giá trị sẽ được điền tự động khi quản trị được lồng trong dự án.

protected function configureFormFields(FormMapper $form): void
{
    $form
        //->add('projets')
        ->add('mediaMedia', \Sonata\AdminBundle\Form\Type\ModelListType::class, [
            'required' => false,
            'btn_add'=>true,
            'btn_edit'=>false,
            'btn_list'=>false,
            'btn_delete'=>false,
        ])
        ->add('ordre')
    ;
}

Và phép màu xảy ra. Bây giờ chúng ta có thể thêm/xóa phương tiện truyền thông trực tiếp trong giao diện của mình.

Nhưng chưa hoàn toàn xong. Chúng ta có một trường để xác định thứ tự hiển thị của mình. Chúng ta chỉ cần cấu hình giao diện quản trị ProjetsMedia để sửa đổi loại của trường thứ tự.

use Symfony\Component\Form\Extension\Core\Type\HiddenType;
...
...
protected function configureFormFields(FormMapper $form): void
{
    $form
        //->add('projets')
        ->add('mediaMedia', ModelListType::class, [
            'required' => false,
            'btn_add'=>true,
            'btn_edit'=>false,
            'btn_list'=>false,
            'btn_delete'=>false,
        ])
        ->add('ordre', HiddenType::class)
    ;
}

Sau đó chúng ta cấu hình Bộ sưu tập trong Projets, để tham số sắp xếp chỉ đến trường chính xác của chúng ta.

$form->add('documents', \Sonata\Form\Type\CollectionType::class,
    [
        'required' => false,
        'by_reference' => false,
        'label'=> false],
    [
        'edit' => 'inline',
        'inline' => 'table',
        'sortable' => 'ordre',
        'link_parameters' => [
            'context' => 'default',
            'provider' => 'sonata.media.provider.file',
            'hide_context' => true
        ],
        'admin_code' => 'admin.projets_medias'
    ]
);

Và cuối cùng, chúng tôi chỉnh sửa mối quan hệ Projets->ProjetsMedia để thiết lập thứ tự mong muốn.

/**
 * @var \Doctrine\Common\Collections\Collection
 *
 * @ORM\OneToMany(targetEntity="App\Entity\ProjetsMedias", mappedBy="projets", cascade={"persist", "remove" })
 * @ORM\JoinTable(name="projets_medias",
 *   joinColumns={
 *     @ORM\JoinColumn(name="projets_id", referencedColumnName="id")
 *   }
 * )
 * @ORM\OrderBy({"ordre" = "ASC"})
 */
private Collection $documents;

Bộ sưu tập của chúng tôi giờ đây có thể được sắp xếp chỉ bằng cách kéo và thả.