ワンクリックで
add-images
Add a multiple images collection (OneToMany) to an existing Sylius Resource
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Add a multiple images collection (OneToMany) to an existing Sylius Resource
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Implement the fix described in a GitHub issue and open a Pull Request that closes it
Apply the latest review feedback to a Pull Request and push it to the PR branch
Add an admin autocomplete form type for a Sylius Resource (translatable or not), optionally injected into another form via an extension
Add an admin FormType for an existing Sylius Resource
Add a Sylius admin grid for an existing Sylius Resource
Add a multiple images collection (OneToMany) to an existing Sylius Resource
| name | add-images |
| description | Add a multiple images collection (OneToMany) to an existing Sylius Resource |
| argument-hint | [ModelName] |
| allowed-tools | AskUserQuestion, Bash, Read, Edit, Write, Glob, Grep |
Ask the user for the ModelName if not provided.
Prerequisite: /sylius-plugin:add-model must have been run first.
src/Entity/{ModelName}/{ModelName}Image.php:
<?php
declare(strict_types=1);
namespace $SYLIUS_NAMESPACE\Entity\{ModelName};
use Doctrine\ORM\Mapping as ORM;
use Sylius\Component\Core\Model\Image;
#[ORM\Entity]
#[ORM\Table(name: '${SYLIUS_PREFIX}_{model_snake}_image')]
class {ModelName}Image extends Image
{
#[ORM\ManyToOne(
targetEntity: {ModelName}::class,
inversedBy: 'images'
)]
#[ORM\JoinColumn(
name: 'owner_id',
referencedColumnName: 'id',
nullable: false,
onDelete: 'CASCADE'
)]
protected $owner = null;
}
Add ImagesAwareInterface and the images collection to the existing entity:
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Sylius\Component\Core\Model\ImageInterface;
use Sylius\Component\Core\Model\ImagesAwareInterface;
class {ModelName} implements {ModelName}Interface, ImagesAwareInterface
{
#[ORM\OneToMany(
targetEntity: {ModelName}Image::class,
mappedBy: 'owner',
orphanRemoval: true,
cascade: ['persist', 'remove', 'detach']
)]
private Collection $images;
public function __construct()
{
// keep existing constructor content
$this->images = new ArrayCollection();
}
public function getImages(): Collection
{
return $this->images;
}
public function getImagesByType(string $type): Collection
{
return $this->images->filter(fn(ImageInterface $image) => $image->getType() === $type);
}
public function hasImages(): bool
{
return !$this->images->isEmpty();
}
public function hasImage(ImageInterface $image): bool
{
return $this->images->contains($image);
}
public function addImage(ImageInterface $image): void
{
if (!$this->hasImage($image)) {
$image->setOwner($this);
$this->images->add($image);
}
}
public function removeImage(ImageInterface $image): void
{
if ($this->hasImage($image)) {
$image->setOwner(null);
$this->images->removeElement($image);
}
}
}
src/Form/Type/{ModelName}/{ModelName}ImageType.php:
<?php
declare(strict_types=1);
namespace $SYLIUS_NAMESPACE\Form\Type\{ModelName};
use $SYLIUS_NAMESPACE\Entity\{ModelName}\{ModelName}Image;
use Sylius\Bundle\CoreBundle\Form\Type\ImageType;
final class {ModelName}ImageType extends ImageType
{
public function __construct()
{
parent::__construct({ModelName}Image::class, ['sylius']);
}
public function getBlockPrefix(): string
{
return '${SYLIUS_PREFIX}_{model_snake}_image';
}
}
config/services.yamlservices:
${SYLIUS_PREFIX}.form.type.{model_snake}_image:
class: $SYLIUS_NAMESPACE\Form\Type\{ModelName}\{ModelName}ImageType
tags:
- { name: form.type }
${SYLIUS_PREFIX}.listener.images_upload.{model_snake}:
parent: sylius.listener.images_upload
autowire: true
public: false
tags:
- { name: kernel.event_listener, event: '${SYLIUS_PREFIX}.{model_snake}.pre_create', method: uploadImages }
- { name: kernel.event_listener, event: '${SYLIUS_PREFIX}.{model_snake}.pre_update', method: uploadImages }
The ResourceFormComponent is required for LiveCollectionType add/remove buttons to work — it wraps the form in a Live Component context. Register it only if it doesn't already exist:
vendor/bin/console debug:container --tag=sylius.live_component.admin | grep '${SYLIUS_PREFIX}:{model_snake}:form'
If the command returns nothing, append to config/services.yaml:
services:
${SYLIUS_PREFIX}.twig.component.{model_snake}.form:
class: Sylius\Bundle\UiBundle\Twig\Component\ResourceFormComponent
arguments:
- '@${SYLIUS_PREFIX}.repository.{model_snake}'
- '@form.factory'
- '%${SYLIUS_PREFIX}.model.{model_snake}.class%'
- $SYLIUS_NAMESPACE\Form\Type\{ModelName}\{ModelName}Type
tags:
- { name: sylius.live_component.admin, key: '${SYLIUS_PREFIX}:{model_snake}:form' }
The event names use
${SYLIUS_PREFIX}(Sylius 2.x resource events use the application prefix, notsylius.*).
Create config/resources/{model_snake}_image.yaml at the plugin root (the image is a separate resource from its owner — one file per resource convention):
sylius_resource:
resources:
${SYLIUS_PREFIX}.{model_snake}_image:
classes:
model: $SYLIUS_NAMESPACE\Entity\{ModelName}\{ModelName}Image
form: $SYLIUS_NAMESPACE\Form\Type\{ModelName}\{ModelName}ImageType
images field to the main FormTypeIn the existing {ModelName}Type, add the images collection:
use Symfony\UX\LiveComponent\Form\Type\LiveCollectionType;
$builder->add('images', LiveCollectionType::class, [
'entry_type' => {ModelName}ImageType::class,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'label' => 'sylius.ui.images',
]);
templates/admin/{model_snake}/form/sections/images.html.twig:
<div class="card mb-3">
<div class="card-header">
<div class="card-title">{{ 'sylius.ui.images'|trans }}</div>
</div>
<div class="card-body">
{% hook 'images' %}
</div>
</div>
templates/admin/{model_snake}/form/sections/images/content.html.twig:
{% set images = hookable_metadata.context.form.images %}
<div class="row">
{% for image_form in images %}
<div class="col-12 col-md-6 row mb-4">
<div class="col-auto">
<div>
{% if image_form.vars.value.path is not null %}
<span class="avatar avatar-xl" style="background-image: url('{{ image_form.vars.value.path|imagine_filter('sylius_small') }}')"></span>
{% else %}
<span class="avatar avatar-xl"></span>
{% endif %}
</div>
<div class="mt-3 d-flex items-center">
{{ form_widget(image_form.vars.button_delete, { label: 'sylius.ui.delete'|trans, attr: { class: 'btn btn-outline-danger w-100' }}) }}
</div>
</div>
<div class="col">
<div class="mb-3">
{{ form_row(image_form.file) }}
</div>
</div>
</div>
{% endfor %}
</div>
templates/admin/{model_snake}/form/sections/images/add_button.html.twig:
<div class="d-grid gap-2">
{{ form_widget(hookable_metadata.context.form.images.vars.button_add) }}
</div>
Hook files are split by action (create.yaml, update.yaml), one directory per resource — matching Sylius core plugin convention (config/twig_hooks/admin/{model_snake}/).
Unified path at the plugin root: config/twig_hooks/admin/{model_snake}/create.yaml and update.yaml.
First-time setup: add the import once to the plugin's config/config.yaml: - { resource: "twig_hooks/**/*.yaml" }.
If
create.yamlalready exists for this resource (e.g. fromadd-translatable-model), theform:block is already set — only add theimages:entry undercontent.form.sectionsand theimages.*sub-hook.
sylius_twig_hooks:
hooks:
'sylius_admin.{model_snake}.create.content':
form:
component: '${SYLIUS_PREFIX}:{model_snake}:form'
props:
resource: '@=_context.resource'
form: '@=_context.form'
template: '@SyliusAdmin/shared/crud/common/content/form.html.twig'
priority: 0
'sylius_admin.{model_snake}.create.content.form.sections':
images:
template: '${SYLIUS_TEMPLATE_NS}admin/{model_snake}/form/sections/images.html.twig'
priority: -100
'sylius_admin.{model_snake}.create.content.form.sections.images':
content:
template: '${SYLIUS_TEMPLATE_NS}admin/{model_snake}/form/sections/images/content.html.twig'
priority: 100
add_button:
template: '${SYLIUS_TEMPLATE_NS}admin/{model_snake}/form/sections/images/add_button.html.twig'
priority: 0
Same structure as create.yaml — replace every occurrence of .create. with .update..
On the image entity:
use Symfony\Component\Validator\Constraints as Assert;
#[Assert\Image(
groups: ['sylius'],
mimeTypes: ['image/png', 'image/jpeg', 'image/gif'],
maxSize: '10M'
)]
protected $file;
On the main entity:
use Symfony\Component\Validator\Constraints as Assert;
#[Assert\Valid]
private Collection $images;
The plugin's DI extension declares the DoctrineMigrations namespace via PrependDoctrineMigrationsTrait, so generate into it:
vendor/bin/console doctrine:migrations:diff --namespace=DoctrineMigrations
Always review the generated migration before applying. doctrine:migrations:diff captures every difference between mapping and DB — including pre-existing schema drift unrelated to your image collection. The migration should only contain CREATE TABLE ${SYLIUS_PREFIX}_{model_snake}_image with its owner_id FK + indexes. If unrelated SQL is present (e.g. on messenger_messages or other Sylius core tables), trim the migration to keep only the image table — drift belongs to the project baseline, not your skill output.
Apply:
vendor/bin/console doctrine:migrations:migrate --no-interaction
vendor/bin/console cache:clear
vendor/bin/console sylius:debug:resource '$SYLIUS_NAMESPACE\Entity\{ModelName}\{ModelName}Image' prints the image resource metadata (alias ${SYLIUS_PREFIX}.{model_snake}_image, model class, form type)vendor/bin/console doctrine:query:sql "DESCRIBE ${SYLIUS_PREFIX}_{model_snake}_image" lists the expected columns (id, owner_id, type, path)vendor/bin/console debug:container --tag=sylius.live_component.admin | grep '${SYLIUS_PREFIX}:{model_snake}:form' finds the live component key