diff --git a/.github/workflows/recipe.yaml b/.github/workflows/recipe.yaml index 9d83f70..a4077d9 100644 --- a/.github/workflows/recipe.yaml +++ b/.github/workflows/recipe.yaml @@ -9,15 +9,13 @@ jobs: recipe: - name: Flex recipe (PHP ${{ matrix.php }}, Sylius ${{ matrix.sylius }}) - runs-on: ubuntu-latest strategy: fail-fast: false matrix: - php: ['8.2'] - sylius: ["~1.13.0"] + php: ['8.1', '8.2'] + sylius: ["~1.12.0", "~1.13.0"] steps: - name: Setup PHP diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml index 8167db5..82777fc 100644 --- a/.github/workflows/security.yaml +++ b/.github/workflows/security.yaml @@ -8,14 +8,12 @@ jobs: security: - name: Security check (PHP ${{ matrix.php }}) - runs-on: ubuntu-latest strategy: fail-fast: false matrix: - php: ['8.2'] + php: ['8.1', '8.2'] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 501ea6c..78b94f4 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -9,14 +9,12 @@ jobs: php: - name: Quality tests (PHP ${{ matrix.php }}) - runs-on: ubuntu-latest strategy: fail-fast: false matrix: - php: ['8.2'] + php: ['8.1', '8.2'] env: COMPOSER_ARGS: --prefer-dist diff --git a/README.md b/README.md index b9e4de6..58f6e0b 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,67 @@ -# Sylius Blog Plugin +

Sylius Blog Plugin

+This plugin adds a blog to your Sylius project. It allows you to create blog articles, tags and authors. +## Compatibility + +## Compatibility + +| Sylius Version | PHP Version | +|---|---| +| 1.12 | 8.1 - 8.2 | +| 1.13 | 8.1 - 8.2 | ## Installation -TBD +If you want to use our recipes, you can add recipes endpoints to your composer.json by running this command: + +```bash +composer config --no-plugins --json extra.symfony.endpoint '["https://api.github.com/repos/monsieurbiz/symfony-recipes/contents/index.json?ref=flex/master","flex://defaults"]' +``` + +Install the plugin via composer: + +```bash +composer require monsieurbiz/sylius-blog-plugin:dev-master +``` + + + +Change your `config/bundles.php` file to add this line for the plugin declaration: + +```php + ['all' => true], +]; +``` + +Add the plugin's routing by creating a new file in `config/routes/monsieurbiz_sylius_blog_plugin.yaml` with the following content: + +```yaml +imports: + resource: '@MonsieurBizSyliusBlogPlugin/Resources/config/config.yaml' +``` + +Add the plugin's routing by creating a new file in `config/routes/monsieurbiz_sylius_blog_plugin.yaml` with the following content: + +```yaml +monsieurbiz_blog_plugin: + resource: '@MonsieurBizSyliusBlogPlugin/Resources/config/routes.yaml' +``` + +And finally, update your database: + +```bash +bin/console doctrine:migrations:migrate +``` + ## License This plugin is under the MIT license. -Please see the [LICENSE](LICENSE) file for more information. +Please see the [LICENSE](LICENSE) file for more information._ diff --git a/composer.json b/composer.json index fc63d11..c469139 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,9 @@ "license": "MIT", "require": { "php": "^8.2", - "sylius/sylius": "^1.12.0 || ^1.13.0" + "sylius/sylius": "^1.12.0 || ^1.13.0", + "monsieurbiz/sylius-rich-editor-plugin": "^2.8", + "monsieurbiz/sylius-media-manager-plugin": "^1.1" }, "require-dev": { "behat/behat": "^3.6.1", diff --git a/dist/.env.local b/dist/.env.local new file mode 100644 index 0000000..2587391 --- /dev/null +++ b/dist/.env.local @@ -0,0 +1,2 @@ +SYLIUS_FIXTURES_HOSTNAME=${SYMFONY_DEFAULT_ROUTE_HOST:-localhost} +SYMFONY_IDE=phpstorm diff --git a/dist/config/routes/monsieurbiz_sylius_blog_plugin.yaml b/dist/config/routes/monsieurbiz_sylius_blog_plugin.yaml index 1fb0cf9..7917a38 100644 --- a/dist/config/routes/monsieurbiz_sylius_blog_plugin.yaml +++ b/dist/config/routes/monsieurbiz_sylius_blog_plugin.yaml @@ -1,2 +1,2 @@ -imports: +monsieurbiz_blog_plugin: resource: '@MonsieurBizSyliusBlogPlugin/Resources/config/routes.yaml' diff --git a/docker-compose.yaml.dist b/docker-compose.yaml.dist index ee0776f..24d6f0c 100644 --- a/docker-compose.yaml.dist +++ b/docker-compose.yaml.dist @@ -1,4 +1,3 @@ -version: '3.8' services: database: image: mysql:8.0 @@ -18,4 +17,4 @@ services: - 1080 volumes: - database: {} \ No newline at end of file + database: {} diff --git a/src/Controller/.gitignore b/src/Controller/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/src/DependencyInjection/MonsieurBizSyliusBlogExtension.php b/src/DependencyInjection/MonsieurBizSyliusBlogExtension.php index b89f91a..ecb8f34 100644 --- a/src/DependencyInjection/MonsieurBizSyliusBlogExtension.php +++ b/src/DependencyInjection/MonsieurBizSyliusBlogExtension.php @@ -18,33 +18,22 @@ use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; -/** - * @SuppressWarnings(PHPMD.LongClassName) - */ final class MonsieurBizSyliusBlogExtension extends Extension implements PrependExtensionInterface { use PrependDoctrineMigrationsTrait; /** - * @inheritdoc + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function load(array $config, ContainerBuilder $container): void + public function load(array $configs, ContainerBuilder $container): void { $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $loader->load('services.yaml'); } - /** - * @inheritdoc - */ public function getAlias(): string { - return 'monsieurbiz_blog'; - } - - public function prepend(ContainerBuilder $container): void - { - $this->prependDoctrineMigrations($container); + return str_replace('monsieur_biz', 'monsieurbiz', parent::getAlias()); } protected function getMigrationsNamespace(): string @@ -63,4 +52,9 @@ protected function getNamespacesOfMigrationsExecutedBefore(): array 'Sylius\Bundle\CoreBundle\Migrations', ]; } + + public function prepend(ContainerBuilder $container): void + { + $this->prependDoctrineMigrations($container); + } } diff --git a/src/Entity/.gitignore b/src/Entity/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/src/Entity/Article.php b/src/Entity/Article.php new file mode 100644 index 0000000..5f1f18f --- /dev/null +++ b/src/Entity/Article.php @@ -0,0 +1,226 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Entity; + +use DateTime; +use DateTimeInterface; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Sylius\Component\Channel\Model\ChannelInterface; +use Sylius\Component\Resource\Model\TimestampableTrait; +use Sylius\Component\Resource\Model\ToggleableTrait; +use Sylius\Component\Resource\Model\TranslatableTrait; + +/** + * @method ArticleTranslationInterface doGetTranslation(?string $locale = null) + */ +class Article implements ArticleInterface +{ + use TimestampableTrait; + use ToggleableTrait; + use TranslatableTrait { + TranslatableTrait::__construct as private initializeTranslationsCollection; + TranslatableTrait::getTranslation as private doGetTranslation; + } + + private ?int $id = null; + + protected ?string $image = null; + + /** @var bool */ + protected $enabled = true; + + /** @var Collection */ + protected Collection $channels; + + /** @var Collection */ + protected Collection $tags; + + protected ?DateTimeInterface $publishedAt; + + /** @var Collection */ + protected Collection $authors; + + protected string $state = ArticleInterface::STATE_DRAFT; + + public function __construct() + { + $this->initializeTranslationsCollection(); + $this->channels = new ArrayCollection(); + $this->tags = new ArrayCollection(); + $this->authors = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getTitle(): ?string + { + return $this->getTranslation()->getTitle(); + } + + public function setTitle(?string $title): void + { + $this->getTranslation()->setTitle($title); + } + + public function getImage(): ?string + { + return $this->image; + } + + public function setImage(?string $image): void + { + $this->image = $image; + } + + public function getSlug(): ?string + { + return $this->getTranslation()->getSlug(); + } + + public function setSlug(?string $slug): void + { + $this->getTranslation()->setSlug($slug); + } + + public function getDescription(): ?string + { + return $this->getTranslation()->getDescription(); + } + + public function setDescription(?string $description): void + { + $this->getTranslation()->setDescription($description); + } + + public function getContent(): ?string + { + return $this->getTranslation()->getContent(); + } + + public function setContent(string $content): void + { + $this->getTranslation()->setContent($content); + } + + public function addTag(TagInterface $tag): void + { + if (!$this->hasTag($tag)) { + $this->tags->add($tag); + $tag->addArticle($this); + } + } + + public function removeTag(TagInterface $tag): void + { + if ($this->hasTag($tag)) { + $this->tags->removeElement($tag); + $tag->removeArticle($this); + } + } + + public function hasTag(TagInterface $tag): bool + { + return $this->tags->contains($tag); + } + + public function getTags(): Collection + { + return $this->tags; + } + + public function getChannels(): Collection + { + return $this->channels; + } + + public function hasChannel(ChannelInterface $channel): bool + { + return $this->channels->contains($channel); + } + + public function addChannel(ChannelInterface $channel): void + { + if (!$this->hasChannel($channel)) { + $this->channels->add($channel); + } + } + + public function removeChannel(ChannelInterface $channel): void + { + if ($this->hasChannel($channel)) { + $this->channels->removeElement($channel); + } + } + + public function getState(): string + { + return $this->state; + } + + public function setState(string $state): void + { + $this->state = $state; + } + + public function getPublishedAt(): ?DateTimeInterface + { + return $this->publishedAt; + } + + public function setPublishedAt(?DateTimeInterface $publishedAt): void + { + $this->publishedAt = $publishedAt; + } + + public function addAuthor(AuthorInterface $author): void + { + if (!$this->hasAuthor($author)) { + $this->authors->add($author); + } + } + + public function removeAuthor(AuthorInterface $author): void + { + if ($this->hasAuthor($author)) { + $this->authors->removeElement($author); + } + } + + public function hasAuthor(AuthorInterface $author): bool + { + return $this->authors->contains($author); + } + + public function getAuthors(): Collection + { + return $this->authors; + } + + public function publish(): void + { + $this->publishedAt = new DateTime(); + } + + public function getTranslation(?string $locale = null): ArticleTranslationInterface + { + return $this->doGetTranslation($locale); + } + + protected function createTranslation(): ArticleTranslationInterface + { + return new ArticleTranslation(); + } +} diff --git a/src/Entity/ArticleInterface.php b/src/Entity/ArticleInterface.php new file mode 100644 index 0000000..b4591e1 --- /dev/null +++ b/src/Entity/ArticleInterface.php @@ -0,0 +1,80 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Entity; + +use DateTimeInterface; +use Doctrine\Common\Collections\Collection; +use Sylius\Component\Channel\Model\ChannelsAwareInterface; +use Sylius\Component\Resource\Model\ResourceInterface; +use Sylius\Component\Resource\Model\SlugAwareInterface; +use Sylius\Component\Resource\Model\TimestampableInterface; +use Sylius\Component\Resource\Model\ToggleableInterface; +use Sylius\Component\Resource\Model\TranslatableInterface; + +interface ArticleInterface extends ChannelsAwareInterface, ResourceInterface, SlugAwareInterface, ToggleableInterface, TranslatableInterface, TimestampableInterface +{ + public const GRAPH = 'monsieurbiz_blog_article'; + + public const TRANSITION_PUBLISH = 'publish'; + + public const STATE_DRAFT = 'draft'; + + public const STATE_PUBLISHED = 'published'; + + public function getTitle(): ?string; + + public function setTitle(?string $title): void; + + public function getDescription(): ?string; + + public function setDescription(?string $description): void; + + public function getImage(): ?string; + + public function setImage(?string $image): void; + + public function getContent(): ?string; + + public function setContent(string $content): void; + + public function addTag(TagInterface $tag): void; + + public function removeTag(TagInterface $tag): void; + + public function hasTag(TagInterface $tag): bool; + + /** + * @return Collection + */ + public function getTags(): Collection; + + public function getState(): string; + + public function setState(string $state): void; + + public function getPublishedAt(): ?DateTimeInterface; + + public function setPublishedAt(?DateTimeInterface $publishedAt): void; + + public function addAuthor(AuthorInterface $author): void; + + public function removeAuthor(AuthorInterface $author): void; + + public function hasAuthor(AuthorInterface $author): bool; + + /** + * @return Collection + */ + public function getAuthors(): Collection; + + public function publish(): void; +} diff --git a/src/Entity/ArticleTranslation.php b/src/Entity/ArticleTranslation.php new file mode 100644 index 0000000..e0c37de --- /dev/null +++ b/src/Entity/ArticleTranslation.php @@ -0,0 +1,75 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Entity; + +use Sylius\Component\Resource\Model\AbstractTranslation; +use Sylius\Component\Resource\Model\TimestampableTrait; + +class ArticleTranslation extends AbstractTranslation implements ArticleTranslationInterface +{ + use TimestampableTrait; + + protected ?int $id; + + protected ?string $title = null; + + protected ?string $slug; + + protected ?string $description; + + protected ?string $content; + + public function getId(): ?int + { + return $this->id; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getSlug(): ?string + { + return $this->slug; + } + + public function setSlug(?string $slug): void + { + $this->slug = $slug; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): void + { + $this->description = $description; + } + + public function getContent(): ?string + { + return $this->content; + } + + public function setContent(?string $content): void + { + $this->content = $content; + } +} diff --git a/src/Entity/ArticleTranslationInterface.php b/src/Entity/ArticleTranslationInterface.php new file mode 100644 index 0000000..f94bc24 --- /dev/null +++ b/src/Entity/ArticleTranslationInterface.php @@ -0,0 +1,32 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Entity; + +use Sylius\Component\Resource\Model\ResourceInterface; +use Sylius\Component\Resource\Model\SlugAwareInterface; +use Sylius\Component\Resource\Model\TimestampableInterface; +use Sylius\Component\Resource\Model\TranslationInterface; + +interface ArticleTranslationInterface extends ResourceInterface, SlugAwareInterface, TimestampableInterface, TranslationInterface +{ + public function getTitle(): ?string; + + public function setTitle(?string $title): void; + + public function getDescription(): ?string; + + public function setDescription(?string $description): void; + + public function getContent(): ?string; + + public function setContent(string $content): void; +} diff --git a/src/Entity/Author.php b/src/Entity/Author.php new file mode 100644 index 0000000..3efc48a --- /dev/null +++ b/src/Entity/Author.php @@ -0,0 +1,39 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Entity; + +class Author implements AuthorInterface +{ + private ?int $id = null; + + protected ?string $name = null; + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(?string $name): void + { + $this->name = $name; + } + + public function __toString(): string + { + return $this->getName() ?? ''; + } +} diff --git a/src/Entity/AuthorInterface.php b/src/Entity/AuthorInterface.php new file mode 100644 index 0000000..3463900 --- /dev/null +++ b/src/Entity/AuthorInterface.php @@ -0,0 +1,23 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Entity; + +use Sylius\Component\Resource\Model\ResourceInterface; + +interface AuthorInterface extends ResourceInterface +{ + public function getId(): ?int; + + public function getName(): ?string; + + public function setName(?string $name): void; +} diff --git a/src/Entity/Tag.php b/src/Entity/Tag.php new file mode 100644 index 0000000..adb4155 --- /dev/null +++ b/src/Entity/Tag.php @@ -0,0 +1,118 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Entity; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Sylius\Component\Resource\Model\TimestampableTrait; +use Sylius\Component\Resource\Model\ToggleableTrait; +use Sylius\Component\Resource\Model\TranslatableTrait; + +class Tag implements TagInterface +{ + use TimestampableTrait; + use ToggleableTrait; + use TranslatableTrait { + TranslatableTrait::__construct as private initializeTranslationsCollection; + TranslatableTrait::getTranslation as private doGetTranslation; + } + + private ?int $id = null; + + /** + * @var bool + */ + protected $enabled = true; + + protected ?int $position = null; + + /** + * @var Collection + */ + protected Collection $articles; + + public function __construct() + { + $this->initializeTranslationsCollection(); + $this->articles = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): ?string + { + return $this->getTranslation()->getName(); + } + + public function setName(?string $name): void + { + $this->getTranslation()->setName($name); + } + + public function getPosition(): ?int + { + return $this->position; + } + + public function setPosition(?int $position): void + { + $this->position = $position; + } + + public function getSlug(): ?string + { + return $this->getTranslation()->getSlug(); + } + + public function setSlug(?string $slug): void + { + $this->getTranslation()->setSlug($slug); + } + + public function addArticle(ArticleInterface $article): void + { + if (!$this->hasArticle($article)) { + $this->articles->add($article); + } + } + + public function removeArticle(ArticleInterface $article): void + { + if ($this->hasArticle($article)) { + $this->articles->removeElement($article); + } + } + + public function hasArticle(ArticleInterface $article): bool + { + return $this->articles->contains($article); + } + + public function getArticles(): Collection + { + return $this->articles; + } + + public function getTranslation(?string $locale = null): TagTranslationInterface + { + /** @phpstan-ignore-next-line */ + return $this->doGetTranslation($locale); + } + + protected function createTranslation(): TagTranslationInterface + { + return new TagTranslation(); + } +} diff --git a/src/Entity/TagInterface.php b/src/Entity/TagInterface.php new file mode 100644 index 0000000..97f1eb0 --- /dev/null +++ b/src/Entity/TagInterface.php @@ -0,0 +1,40 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Entity; + +use Doctrine\Common\Collections\Collection; +use Sylius\Component\Resource\Model\ResourceInterface; +use Sylius\Component\Resource\Model\SlugAwareInterface; +use Sylius\Component\Resource\Model\ToggleableInterface; +use Sylius\Component\Resource\Model\TranslatableInterface; + +interface TagInterface extends ResourceInterface, ToggleableInterface, TranslatableInterface, SlugAwareInterface +{ + public function getName(): ?string; + + public function setName(?string $name): void; + + public function getPosition(): ?int; + + public function setPosition(?int $position): void; + + public function addArticle(ArticleInterface $article): void; + + public function removeArticle(ArticleInterface $article): void; + + public function hasArticle(ArticleInterface $article): bool; + + /** + * @return Collection + */ + public function getArticles(): Collection; +} diff --git a/src/Entity/TagTranslation.php b/src/Entity/TagTranslation.php new file mode 100644 index 0000000..f35b56b --- /dev/null +++ b/src/Entity/TagTranslation.php @@ -0,0 +1,51 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Entity; + +use Sylius\Component\Resource\Model\AbstractTranslation; +use Sylius\Component\Resource\Model\TimestampableTrait; + +class TagTranslation extends AbstractTranslation implements TagTranslationInterface +{ + use TimestampableTrait; + + protected ?int $id; + + protected ?string $name = null; + + protected ?string $slug; + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(?string $name): void + { + $this->name = $name; + } + + public function getSlug(): ?string + { + return $this->slug; + } + + public function setSlug(?string $slug): void + { + $this->slug = $slug; + } +} diff --git a/src/Entity/TagTranslationInterface.php b/src/Entity/TagTranslationInterface.php new file mode 100644 index 0000000..72642c3 --- /dev/null +++ b/src/Entity/TagTranslationInterface.php @@ -0,0 +1,24 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Entity; + +use Sylius\Component\Resource\Model\ResourceInterface; +use Sylius\Component\Resource\Model\SlugAwareInterface; +use Sylius\Component\Resource\Model\TimestampableInterface; +use Sylius\Component\Resource\Model\TranslationInterface; + +interface TagTranslationInterface extends ResourceInterface, SlugAwareInterface, TimestampableInterface, TranslationInterface +{ + public function getName(): ?string; + + public function setName(?string $name): void; +} diff --git a/src/EventListener/AdminMenuListener.php b/src/EventListener/AdminMenuListener.php new file mode 100644 index 0000000..b6341ae --- /dev/null +++ b/src/EventListener/AdminMenuListener.php @@ -0,0 +1,42 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\EventListener; + +use Sylius\Bundle\UiBundle\Menu\Event\MenuBuilderEvent; + +final class AdminMenuListener +{ + public function __invoke(MenuBuilderEvent $event): void + { + $menu = $event->getMenu(); + + $blogMenu = $menu + ->addChild('monsieurbiz-blog') + ->setLabel('monsieurbiz_blog.ui.menu_blog') + ; + + $blogMenu->addChild('monsieurbiz-blog-tags', ['route' => 'monsieurbiz_blog_admin_tag_index']) + ->setLabel('monsieurbiz_blog.ui.tags') + ->setLabelAttribute('icon', 'grid layout') + ; + + $blogMenu->addChild('monsieurbiz-blog-articles', ['route' => 'monsieurbiz_blog_admin_article_index']) + ->setLabel('monsieurbiz_blog.ui.articles') + ->setLabelAttribute('icon', 'newspaper') + ; + + $blogMenu->addChild('monsieurbiz-blog-authors', ['route' => 'monsieurbiz_blog_admin_author_index']) + ->setLabel('monsieurbiz_blog.ui.authors') + ->setLabelAttribute('icon', 'user') + ; + } +} diff --git a/src/Fixture/ArticleFixture.php b/src/Fixture/ArticleFixture.php new file mode 100644 index 0000000..6718e1f --- /dev/null +++ b/src/Fixture/ArticleFixture.php @@ -0,0 +1,63 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Fixture; + +use Doctrine\ORM\EntityManagerInterface; +use MonsieurBiz\SyliusBlogPlugin\Fixture\Factory\ArticleFixtureFactory; +use Sylius\Bundle\CoreBundle\Fixture\AbstractResourceFixture; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; + +final class ArticleFixture extends AbstractResourceFixture +{ + public function __construct( + EntityManagerInterface $blogArticleManager, + ArticleFixtureFactory $exampleFactory + ) { + parent::__construct($blogArticleManager, $exampleFactory); + } + + public function getName(): string + { + return 'monsieubiz_blog_article'; + } + + protected function configureResourceNode(ArrayNodeDefinition $resourceNode): void + { + /** @phpstan-ignore-next-line */ + $resourceNode + ->children() + ->booleanNode('enabled')->defaultTrue()->end() + ->scalarNode('image')->defaultNull()->end() + ->scalarNode('tag')->defaultNull()->end() + ->scalarNode('is_published')->defaultTrue()->end() + ->scalarNode('publish_date')->cannotBeEmpty()->end() + ->arrayNode('authors') + ->arrayPrototype() + ->children() + ->scalarNode('name')->cannotBeEmpty()->end() + ->end() + ->end() + ->end() + ->arrayNode('translations') + ->arrayPrototype() + ->children() + ->scalarNode('title')->cannotBeEmpty()->end() + ->scalarNode('slug')->cannotBeEmpty()->end() + ->scalarNode('description')->cannotBeEmpty()->end() + ->scalarNode('content')->cannotBeEmpty()->end() + ->end() + ->end() + ->end() + ->end() + ; + } +} diff --git a/src/Fixture/AuthorFixture.php b/src/Fixture/AuthorFixture.php new file mode 100644 index 0000000..ad83b2b --- /dev/null +++ b/src/Fixture/AuthorFixture.php @@ -0,0 +1,42 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Fixture; + +use Doctrine\ORM\EntityManagerInterface; +use MonsieurBiz\SyliusBlogPlugin\Fixture\Factory\AuthorFixtureFactory; +use Sylius\Bundle\CoreBundle\Fixture\AbstractResourceFixture; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; + +final class AuthorFixture extends AbstractResourceFixture +{ + public function __construct( + EntityManagerInterface $blogAuthorManager, + AuthorFixtureFactory $exampleFactory + ) { + parent::__construct($blogAuthorManager, $exampleFactory); + } + + public function getName(): string + { + return 'monsieubiz_blog_author'; + } + + protected function configureResourceNode(ArrayNodeDefinition $resourceNode): void + { + /** @phpstan-ignore-next-line */ + $resourceNode + ->children() + ->scalarNode('name')->cannotBeEmpty()->end() + ->end() + ; + } +} diff --git a/src/Fixture/Factory/ArticleFixtureFactory.php b/src/Fixture/Factory/ArticleFixtureFactory.php new file mode 100644 index 0000000..dba31a1 --- /dev/null +++ b/src/Fixture/Factory/ArticleFixtureFactory.php @@ -0,0 +1,243 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Fixture\Factory; + +use Closure; +use DateTime; +use DateTimeInterface; +use Faker\Factory; +use Faker\Generator; +use MonsieurBiz\SyliusBlogPlugin\Entity\ArticleInterface; +use MonsieurBiz\SyliusBlogPlugin\Entity\ArticleTranslationInterface; +use MonsieurBiz\SyliusBlogPlugin\Entity\AuthorInterface; +use MonsieurBiz\SyliusBlogPlugin\Entity\TagInterface; +use MonsieurBiz\SyliusBlogPlugin\Repository\TagRepositoryInterface; +use MonsieurBiz\SyliusMediaManagerPlugin\Exception\CannotReadCurrentFolderException; +use MonsieurBiz\SyliusMediaManagerPlugin\Helper\FileHelperInterface; +use MonsieurBiz\SyliusMediaManagerPlugin\Model\File; +use SM\Factory\FactoryInterface as StateMachineFactoryInterface; +use Sylius\Bundle\CoreBundle\Fixture\Factory\AbstractExampleFactory; +use Sylius\Bundle\CoreBundle\Fixture\OptionsResolver\LazyOption; +use Sylius\Component\Channel\Repository\ChannelRepositoryInterface; +use Sylius\Component\Core\Formatter\StringInflector; +use Sylius\Component\Core\Model\ChannelInterface; +use Sylius\Component\Locale\Model\LocaleInterface; +use Sylius\Component\Resource\Factory\FactoryInterface; +use Sylius\Component\Resource\Repository\RepositoryInterface; +use Symfony\Component\Config\FileLocatorInterface; +use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +final class ArticleFixtureFactory extends AbstractExampleFactory +{ + private OptionsResolver $optionsResolver; + + private OptionsResolver $translationOptionsResolver; + + private Generator $faker; + + /** + * @param FactoryInterface $articleFactory + * @param FactoryInterface $articleTranslationFactory + * @param TagRepositoryInterface $tagRepository + * @param RepositoryInterface $localeRepository + * @param ChannelRepositoryInterface $channelRepository + * @param RepositoryInterface $authorRepository + */ + public function __construct( + private FactoryInterface $articleFactory, + private FactoryInterface $articleTranslationFactory, + private TagRepositoryInterface $tagRepository, + private StateMachineFactoryInterface $stateMachineFactory, + private RepositoryInterface $localeRepository, + private ChannelRepositoryInterface $channelRepository, + private RepositoryInterface $authorRepository, + private FileLocatorInterface $fileLocator, + private FileHelperInterface $fileHelper, + ) { + $this->faker = Factory::create(); + + $this->optionsResolver = new OptionsResolver(); + $this->configureOptions($this->optionsResolver); + + $this->translationOptionsResolver = new OptionsResolver(); + $this->configureTranslationOptions($this->translationOptionsResolver); + } + + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function create(array $options = []): ArticleInterface + { + $options = $this->optionsResolver->resolve($options); + + /** @var ArticleInterface $article */ + $article = $this->articleFactory->createNew(); + $article->setEnabled($options['enabled']); + $article->addTag($options['tag']); + $article->setImage($options['image']); + $channels = $this->channelRepository->findAll(); + /** @var ChannelInterface $channel */ + foreach ($channels as $channel) { + $article->addChannel($channel); + } + $this->addAuthors($article, $options); + $this->createTranslations($article, $options); + + if ($options['is_published']) { + $this->applyTransition($article, ArticleInterface::TRANSITION_PUBLISH); + } + if (ArticleInterface::STATE_PUBLISHED === $article->getState() && null !== $options['publish_date']) { + $article->setPublishedAt($options['publish_date']); + } + + return $article; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setDefault('enabled', function (Options $options): bool { + return $this->faker->boolean(80); + }) + + ->setDefault('image', $this->lazyImageDefault(80)) + ->setAllowedTypes('image', ['string', 'null']) + + ->setDefault('tag', LazyOption::randomOne($this->tagRepository)) + ->setAllowedTypes('tag', ['string', TagInterface::class]) + ->setNormalizer('tag', function (Options $options, $previousValue): ?object { + if (null === $previousValue || \is_object($previousValue)) { + return $previousValue; + } + + return $this->tagRepository->findOneByName($previousValue, 'fr'); + }) + + ->setDefault('authors', []) + ->setAllowedTypes('authors', ['array']) + + ->setDefault('translations', []) + ->setAllowedTypes('translations', ['array']) + + ->setDefault('is_published', fn (Options $options): bool => $this->faker->boolean(80)) + ->setAllowedTypes('is_published', ['bool']) + + ->setDefault('publish_date', fn (Options $options): DateTimeInterface => $this->faker->dateTimeBetween('-1 years', 'now')) + ->setAllowedTypes('publish_date', ['null', DateTime::class]) + ; + } + + private function addAuthors(ArticleInterface $article, array $options): void + { + foreach ($options['authors'] as $author) { + /** @var ?AuthorInterface $author */ + $author = $this->authorRepository->findOneBy(['name' => $author['name']]); + + if (null !== $author) { + $article->addAuthor($author); + } + } + } + + private function createTranslations(ArticleInterface $article, array $options): void + { + // add translation for each defined locales + foreach ($this->getLocales() as $localeCode) { + $translation = $options['translations'][$localeCode] ?? []; + $translation = $this->translationOptionsResolver->resolve($translation); + /** @var ArticleTranslationInterface $articleTranslation */ + $articleTranslation = $this->articleTranslationFactory->createNew(); + $articleTranslation->setLocale($localeCode); + $articleTranslation->setTitle($translation['title']); + $articleTranslation->setSlug($translation['slug'] ?? StringInflector::nameToCode($translation['title'])); + $articleTranslation->setDescription($translation['description']); + $articleTranslation->setContent($translation['content']); + + $article->addTranslation($articleTranslation); + } + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + private function configureTranslationOptions(OptionsResolver $resolver): void + { + $resolver + ->setDefault('title', fn (Options $options): string => /** @phpstan-ignore-line */ $this->faker->words(3, true)) + ->setDefault('slug', null) + ->setDefault('description', fn (Options $options): string => $this->faker->paragraph) + ->setDefault('content', fn (Options $options): string => $this->faker->paragraph) + ; + } + + private function applyTransition(ArticleInterface $article, string $transition): void + { + $this->stateMachineFactory->get($article, ArticleInterface::GRAPH)->apply($transition); + } + + private function getLocales(): iterable + { + /** @var LocaleInterface[] $locales */ + $locales = $this->localeRepository->findAll(); + foreach ($locales as $locale) { + yield $locale->getCode(); + } + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + private function lazyImageDefault(int $chanceOfRandomOne): Closure + { + return function (Options $options) use ($chanceOfRandomOne): ?string { + if (random_int(1, 100) > $chanceOfRandomOne) { + return null; + } + + $random = random_int(1, 5); + $sourcePath = $this->fileLocator->locate(sprintf('@MonsieurBizSyliusBlogPlugin/Resources/fixtures/article-%d.jpg', $random)); + $existingImage = $this->findExistingImage(basename($sourcePath)); + if (null !== $existingImage) { + return $existingImage; + } + + $file = new UploadedFile($sourcePath, basename($sourcePath)); + $filename = $this->fileHelper->upload($file, 'blog', 'gallery/images'); + + return 'gallery/images/blog/' . $filename; + }; + } + + private function findExistingImage(string $filename): ?string + { + try { + $files = $this->fileHelper->list('blog', 'gallery/images'); + } catch (CannotReadCurrentFolderException) { + $this->fileHelper->createFolder('blog', '', 'gallery/images'); // Create the folder if it does not exist + $files = []; + } + + /** @var File $file */ + foreach ($files as $file) { + if ($filename === $file->getName()) { + return 'gallery/images/' . $file->getPath(); + } + } + + return null; + } +} diff --git a/src/Fixture/Factory/AuthorFixtureFactory.php b/src/Fixture/Factory/AuthorFixtureFactory.php new file mode 100644 index 0000000..eef1d64 --- /dev/null +++ b/src/Fixture/Factory/AuthorFixtureFactory.php @@ -0,0 +1,63 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Fixture\Factory; + +use Faker\Factory; +use Faker\Generator; +use MonsieurBiz\SyliusBlogPlugin\Entity\AuthorInterface; +use Sylius\Bundle\CoreBundle\Fixture\Factory\AbstractExampleFactory; +use Sylius\Component\Resource\Factory\FactoryInterface; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +final class AuthorFixtureFactory extends AbstractExampleFactory +{ + private OptionsResolver $optionsResolver; + + private Generator $faker; + + /** + * @param FactoryInterface $authorFactory + */ + public function __construct( + private FactoryInterface $authorFactory, + ) { + $this->faker = Factory::create(); + + $this->optionsResolver = new OptionsResolver(); + $this->configureOptions($this->optionsResolver); + } + + public function create(array $options = []): AuthorInterface + { + $options = $this->optionsResolver->resolve($options); + + /** @var AuthorInterface $author */ + $author = $this->authorFactory->createNew(); + $author->setName($options['name']); + + return $author; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setDefault('name', function (Options $options) { + return $this->faker->name; + }) + ->setAllowedTypes('name', 'string') + ; + } +} diff --git a/src/Fixture/Factory/TagFixtureFactory.php b/src/Fixture/Factory/TagFixtureFactory.php new file mode 100644 index 0000000..52d5b60 --- /dev/null +++ b/src/Fixture/Factory/TagFixtureFactory.php @@ -0,0 +1,119 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Fixture\Factory; + +use Behat\Transliterator\Transliterator; +use Faker\Factory; +use Faker\Generator; +use MonsieurBiz\SyliusBlogPlugin\Entity\TagInterface; +use MonsieurBiz\SyliusBlogPlugin\Entity\TagTranslationInterface; +use Sylius\Bundle\CoreBundle\Fixture\Factory\AbstractExampleFactory; +use Sylius\Component\Locale\Model\LocaleInterface; +use Sylius\Component\Resource\Factory\FactoryInterface; +use Sylius\Component\Resource\Repository\RepositoryInterface; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +final class TagFixtureFactory extends AbstractExampleFactory +{ + private OptionsResolver $optionsResolver; + + private OptionsResolver $translationOptionsResolver; + + private Generator $faker; + + /** + * @param FactoryInterface $tagFactory + * @param FactoryInterface $tagTranslationFactory + * @param RepositoryInterface $localeRepository + */ + public function __construct( + private FactoryInterface $tagFactory, + private FactoryInterface $tagTranslationFactory, + private RepositoryInterface $localeRepository, + ) { + $this->faker = Factory::create(); + + $this->optionsResolver = new OptionsResolver(); + $this->configureOptions($this->optionsResolver); + + $this->translationOptionsResolver = new OptionsResolver(); + $this->configureTranslationOptions($this->translationOptionsResolver); + } + + public function create(array $options = []): TagInterface + { + $options = $this->optionsResolver->resolve($options); + + /** @var TagInterface $tag */ + $tag = $this->tagFactory->createNew(); + $tag->setEnabled($options['enabled']); + $this->createTranslations($tag, $options); + + return $tag; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setDefault('enabled', function (Options $options): bool { + return $this->faker->boolean(80); + }) + + ->setDefault('translations', function (OptionsResolver $translationResolver): void { + $translationResolver->setDefaults($this->configureDefaultTranslations()); + }) + ->setAllowedTypes('translations', ['array']) + ; + } + + private function createTranslations(TagInterface $tag, array $options): void + { + foreach ($options['translations'] as $localeCode => $translation) { + $translation = $this->translationOptionsResolver->resolve($translation); + /** @var TagTranslationInterface $tagTranslation */ + $tagTranslation = $this->tagTranslationFactory->createNew(); + $tagTranslation->setLocale($localeCode); + $tagTranslation->setName($translation['name']); + $slug = $translation['slug'] ?? Transliterator::transliterate(str_replace('\'', '-', $translation['name'])); + $tagTranslation->setSlug($slug); + + $tag->addTranslation($tagTranslation); + } + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + private function configureTranslationOptions(OptionsResolver $resolver): void + { + $resolver + ->setDefault('name', fn (Options $options): string => /** @phpstan-ignore-line */ $this->faker->words(3, true)) + ->setDefault('slug', null) + ; + } + + private function configureDefaultTranslations(): array + { + $translations = []; + $locales = $this->localeRepository->findAll(); + /** @var LocaleInterface $locale */ + foreach ($locales as $locale) { + $translations[$locale->getCode()] = []; + } + + return $translations; + } +} diff --git a/src/Fixture/TagFixture.php b/src/Fixture/TagFixture.php new file mode 100644 index 0000000..f8df38c --- /dev/null +++ b/src/Fixture/TagFixture.php @@ -0,0 +1,50 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Fixture; + +use Doctrine\ORM\EntityManagerInterface; +use MonsieurBiz\SyliusBlogPlugin\Fixture\Factory\TagFixtureFactory; +use Sylius\Bundle\CoreBundle\Fixture\AbstractResourceFixture; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; + +final class TagFixture extends AbstractResourceFixture +{ + public function __construct( + EntityManagerInterface $blogTagManager, + TagFixtureFactory $exampleFactory + ) { + parent::__construct($blogTagManager, $exampleFactory); + } + + public function getName(): string + { + return 'monsieurbiz_blog_tag'; + } + + protected function configureResourceNode(ArrayNodeDefinition $resourceNode): void + { + /** @phpstan-ignore-next-line */ + $resourceNode + ->children() + ->booleanNode('enabled')->defaultTrue()->end() + ->arrayNode('translations') + ->arrayPrototype() + ->children() + ->scalarNode('name')->cannotBeEmpty()->end() + ->scalarNode('slug')->cannotBeEmpty()->end() + ->end() + ->end() + ->end() + ->end() + ; + } +} diff --git a/src/Form/Type/ArticleTranslationType.php b/src/Form/Type/ArticleTranslationType.php new file mode 100644 index 0000000..3130cdb --- /dev/null +++ b/src/Form/Type/ArticleTranslationType.php @@ -0,0 +1,44 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Form\Type; + +use MonsieurBiz\SyliusRichEditorPlugin\Form\Type\RichEditorType; +use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType; +use Symfony\Component\Form\Extension\Core\Type\TextareaType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; + +final class ArticleTranslationType extends AbstractResourceType +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('title', TextType::class, [ + 'label' => 'monsieurbiz_blog.form.article.title', + 'required' => true, + ]) + ->add('slug', TextType::class, [ + 'label' => 'monsieurbiz_blog.form.article.slug', + 'required' => true, + ]) + ->add('description', TextareaType::class, [ + 'label' => 'monsieurbiz_blog.form.article.description', + ]) + ->add('content', RichEditorType::class, [ + 'label' => 'monsieurbiz_blog.form.article.content', + ]) + ; + } +} diff --git a/src/Form/Type/ArticleType.php b/src/Form/Type/ArticleType.php new file mode 100644 index 0000000..50832b5 --- /dev/null +++ b/src/Form/Type/ArticleType.php @@ -0,0 +1,90 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Form\Type; + +use MonsieurBiz\SyliusBlogPlugin\Entity\AuthorInterface; +use MonsieurBiz\SyliusBlogPlugin\Entity\Tag; +use MonsieurBiz\SyliusBlogPlugin\Entity\TagInterface; +use MonsieurBiz\SyliusBlogPlugin\Repository\AuthorRepositoryInterface; +use MonsieurBiz\SyliusBlogPlugin\Repository\TagRepositoryInterface; +use MonsieurBiz\SyliusMediaManagerPlugin\Form\Type\ImageType; +use Sylius\Bundle\ChannelBundle\Form\Type\ChannelChoiceType; +use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType; +use Sylius\Bundle\ResourceBundle\Form\Type\ResourceTranslationsType; +use Sylius\Component\Locale\Context\LocaleContextInterface; +use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\FormBuilderInterface; + +final class ArticleType extends AbstractResourceType +{ + /** + * @param AuthorRepositoryInterface $authorRepository + */ + public function __construct( + private LocaleContextInterface $localeContext, + private AuthorRepositoryInterface $authorRepository, + string $dataClass, + array $validationGroups = [], + ) { + parent::__construct($dataClass, $validationGroups); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('enabled', CheckboxType::class, [ + 'required' => false, + 'label' => 'sylius.ui.enabled', + ]) + ->add('channels', ChannelChoiceType::class, [ + 'multiple' => true, + 'expanded' => true, + 'label' => 'sylius.form.product.channels', + ]) + ->add('authors', ChoiceType::class, [ + 'label' => 'monsieurbiz_blog.form.article.authors', + 'multiple' => true, + 'choices' => $this->authorRepository->findAll(), + 'choice_label' => 'name', + 'choice_translation_domain' => false, + 'required' => false, + ]) + ->add('tags', EntityType::class, [ + 'label' => 'monsieurbiz_blog.form.article.tags', + 'required' => true, + 'multiple' => true, + 'class' => Tag::class, + 'query_builder' => function (TagRepositoryInterface $tagRepository) { + return $tagRepository->createListQueryBuilder($this->localeContext->getLocaleCode()); + }, + 'choice_label' => function (TagInterface $tag) { + return $tag->getName(); + }, + ]) + ->add('image', ImageType::class, [ + 'label' => 'monsieurbiz_blog.form.article.image', + 'required' => false, + ]) + ->add('translations', ResourceTranslationsType::class, [ + 'entry_type' => ArticleTranslationType::class, + ]) + ; + + $builder->get('authors')->addModelTransformer(new CollectionToArrayTransformer()); + } +} diff --git a/src/Form/Type/AuthorType.php b/src/Form/Type/AuthorType.php new file mode 100644 index 0000000..36adda3 --- /dev/null +++ b/src/Form/Type/AuthorType.php @@ -0,0 +1,32 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Form\Type; + +use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; + +final class AuthorType extends AbstractResourceType +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('name', TextType::class, [ + 'required' => false, + 'label' => 'monsieurbiz_blog.form.author.name', + ]) + ; + } +} diff --git a/src/Form/Type/TagTranslationType.php b/src/Form/Type/TagTranslationType.php new file mode 100644 index 0000000..e3ffb7f --- /dev/null +++ b/src/Form/Type/TagTranslationType.php @@ -0,0 +1,36 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Form\Type; + +use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; + +final class TagTranslationType extends AbstractResourceType +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('name', TextType::class, [ + 'label' => 'monsieurbiz_blog.form.tag.title', + 'required' => true, + ]) + ->add('slug', TextType::class, [ + 'label' => 'monsieurbiz_blog.form.tag.slug', + 'required' => true, + ]) + ; + } +} diff --git a/src/Form/Type/TagType.php b/src/Form/Type/TagType.php new file mode 100644 index 0000000..2725e7f --- /dev/null +++ b/src/Form/Type/TagType.php @@ -0,0 +1,36 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Form\Type; + +use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType; +use Sylius\Bundle\ResourceBundle\Form\Type\ResourceTranslationsType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\FormBuilderInterface; + +final class TagType extends AbstractResourceType +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameters) + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('enabled', CheckboxType::class, [ + 'required' => false, + 'label' => 'sylius.ui.enabled', + ]) + ->add('translations', ResourceTranslationsType::class, [ + 'entry_type' => TagTranslationType::class, + ]) + ; + } +} diff --git a/src/Menu/AdminArticleUpdateMenuBuilder.php b/src/Menu/AdminArticleUpdateMenuBuilder.php new file mode 100644 index 0000000..04a291b --- /dev/null +++ b/src/Menu/AdminArticleUpdateMenuBuilder.php @@ -0,0 +1,58 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Menu; + +use Knp\Menu\FactoryInterface; +use Knp\Menu\ItemInterface; +use MonsieurBiz\SyliusBlogPlugin\Entity\ArticleInterface; +use SM\Factory\FactoryInterface as StateMachineFactoryInterface; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; + +final class AdminArticleUpdateMenuBuilder +{ + public function __construct( + private FactoryInterface $factory, + private StateMachineFactoryInterface $stateMachineFactory, + private CsrfTokenManagerInterface $csrfTokenManager, + ) { + } + + public function createMenu(array $options): ItemInterface + { + $menu = $this->factory->createItem('root'); + + $article = $options['article'] ?? null; + if (!$article instanceof ArticleInterface) { + return $menu; + } + + $stateMachine = $this->stateMachineFactory->get($article, ArticleInterface::GRAPH); + if ($stateMachine->can(ArticleInterface::TRANSITION_PUBLISH)) { + $menu + ->addChild('publish', [ + 'route' => 'monsieurbiz_blog_admin_article_update_state', + 'routeParameters' => [ + 'id' => $article->getId(), + 'state' => ArticleInterface::TRANSITION_PUBLISH, + '_csrf_token' => $this->csrfTokenManager->getToken((string) $article->getId())->getValue(), + ], + ]) + ->setAttribute('type', 'transition') + ->setLabel('monsieurbiz_blog.ui.publish') + ->setLabelAttribute('icon', 'check') + ->setLabelAttribute('color', 'green') + ; + } + + return $menu; + } +} diff --git a/src/Migrations/.gitignore b/src/Migrations/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/src/Migrations/Version20240702145615.php b/src/Migrations/Version20240702145615.php new file mode 100644 index 0000000..10896c7 --- /dev/null +++ b/src/Migrations/Version20240702145615.php @@ -0,0 +1,70 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Migrations; + +use Doctrine\DBAL\Schema\Schema; +use Doctrine\Migrations\AbstractMigration; + +/** + * Auto-generated Migration: Please modify to your needs! + */ +final class Version20240702145615 extends AbstractMigration +{ + public function getDescription(): string + { + return ''; + } + + public function up(Schema $schema): void + { + // this up() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE TABLE monsieurbiz_blog_article (id INT AUTO_INCREMENT NOT NULL, enabled TINYINT(1) DEFAULT 1 NOT NULL, image VARCHAR(255) DEFAULT NULL, state VARCHAR(255) NOT NULL, publishedAt DATETIME DEFAULT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE monsieurbiz_blog_article_tags (article_id INT NOT NULL, tag_id INT NOT NULL, INDEX IDX_81F1F9E97294869C (article_id), INDEX IDX_81F1F9E9BAD26311 (tag_id), PRIMARY KEY(article_id, tag_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE monsieurbiz_blog_article_channels (article_id INT NOT NULL, channel_id INT NOT NULL, INDEX IDX_9F50BAA27294869C (article_id), INDEX IDX_9F50BAA272F5A1AA (channel_id), PRIMARY KEY(article_id, channel_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE monsieurbiz_blog_article_authors (article_id INT NOT NULL, author_id INT NOT NULL, INDEX IDX_CCC1D3057294869C (article_id), INDEX IDX_CCC1D305F675F31B (author_id), PRIMARY KEY(article_id, author_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE monsieurbiz_blog_article_translation (id INT AUTO_INCREMENT NOT NULL, translatable_id INT NOT NULL, title VARCHAR(255) NOT NULL, slug VARCHAR(255) NOT NULL, description LONGTEXT NOT NULL, content LONGTEXT NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME NOT NULL, locale VARCHAR(255) NOT NULL, INDEX IDX_AC951C7A2C2AC5D3 (translatable_id), UNIQUE INDEX monsieurbiz_blog_article_translation_uniq_trans (translatable_id, locale), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE monsieurbiz_blog_author (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE monsieurbiz_blog_tag (id INT AUTO_INCREMENT NOT NULL, enabled TINYINT(1) DEFAULT 1 NOT NULL, position INT NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE monsieurbiz_blog_tag_translation (id INT AUTO_INCREMENT NOT NULL, translatable_id INT NOT NULL, name VARCHAR(255) NOT NULL, slug VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME NOT NULL, locale VARCHAR(255) NOT NULL, INDEX IDX_7BF826C2C2AC5D3 (translatable_id), UNIQUE INDEX monsieurbiz_blog_tag_translation_uniq_trans (translatable_id, locale), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE monsieurbiz_blog_article_tags ADD CONSTRAINT FK_81F1F9E97294869C FOREIGN KEY (article_id) REFERENCES monsieurbiz_blog_article (id)'); + $this->addSql('ALTER TABLE monsieurbiz_blog_article_tags ADD CONSTRAINT FK_81F1F9E9BAD26311 FOREIGN KEY (tag_id) REFERENCES monsieurbiz_blog_tag (id)'); + $this->addSql('ALTER TABLE monsieurbiz_blog_article_channels ADD CONSTRAINT FK_9F50BAA27294869C FOREIGN KEY (article_id) REFERENCES monsieurbiz_blog_article (id)'); + $this->addSql('ALTER TABLE monsieurbiz_blog_article_channels ADD CONSTRAINT FK_9F50BAA272F5A1AA FOREIGN KEY (channel_id) REFERENCES sylius_channel (id)'); + $this->addSql('ALTER TABLE monsieurbiz_blog_article_authors ADD CONSTRAINT FK_CCC1D3057294869C FOREIGN KEY (article_id) REFERENCES monsieurbiz_blog_article (id)'); + $this->addSql('ALTER TABLE monsieurbiz_blog_article_authors ADD CONSTRAINT FK_CCC1D305F675F31B FOREIGN KEY (author_id) REFERENCES monsieurbiz_blog_author (id)'); + $this->addSql('ALTER TABLE monsieurbiz_blog_article_translation ADD CONSTRAINT FK_AC951C7A2C2AC5D3 FOREIGN KEY (translatable_id) REFERENCES monsieurbiz_blog_article (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE monsieurbiz_blog_tag_translation ADD CONSTRAINT FK_7BF826C2C2AC5D3 FOREIGN KEY (translatable_id) REFERENCES monsieurbiz_blog_tag (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE messenger_messages CHANGE created_at created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', CHANGE available_at available_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', CHANGE delivered_at delivered_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE monsieurbiz_blog_article_tags DROP FOREIGN KEY FK_81F1F9E97294869C'); + $this->addSql('ALTER TABLE monsieurbiz_blog_article_tags DROP FOREIGN KEY FK_81F1F9E9BAD26311'); + $this->addSql('ALTER TABLE monsieurbiz_blog_article_channels DROP FOREIGN KEY FK_9F50BAA27294869C'); + $this->addSql('ALTER TABLE monsieurbiz_blog_article_channels DROP FOREIGN KEY FK_9F50BAA272F5A1AA'); + $this->addSql('ALTER TABLE monsieurbiz_blog_article_authors DROP FOREIGN KEY FK_CCC1D3057294869C'); + $this->addSql('ALTER TABLE monsieurbiz_blog_article_authors DROP FOREIGN KEY FK_CCC1D305F675F31B'); + $this->addSql('ALTER TABLE monsieurbiz_blog_article_translation DROP FOREIGN KEY FK_AC951C7A2C2AC5D3'); + $this->addSql('ALTER TABLE monsieurbiz_blog_tag_translation DROP FOREIGN KEY FK_7BF826C2C2AC5D3'); + $this->addSql('DROP TABLE monsieurbiz_blog_article'); + $this->addSql('DROP TABLE monsieurbiz_blog_article_tags'); + $this->addSql('DROP TABLE monsieurbiz_blog_article_channels'); + $this->addSql('DROP TABLE monsieurbiz_blog_article_authors'); + $this->addSql('DROP TABLE monsieurbiz_blog_article_translation'); + $this->addSql('DROP TABLE monsieurbiz_blog_author'); + $this->addSql('DROP TABLE monsieurbiz_blog_tag'); + $this->addSql('DROP TABLE monsieurbiz_blog_tag_translation'); + $this->addSql('ALTER TABLE messenger_messages CHANGE created_at created_at DATETIME NOT NULL, CHANGE available_at available_at DATETIME NOT NULL, CHANGE delivered_at delivered_at DATETIME DEFAULT NULL'); + } +} diff --git a/src/Repository/ArticleRepository.php b/src/Repository/ArticleRepository.php new file mode 100644 index 0000000..d91fa7c --- /dev/null +++ b/src/Repository/ArticleRepository.php @@ -0,0 +1,115 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Repository; + +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\QueryBuilder; +use MonsieurBiz\SyliusBlogPlugin\Entity\ArticleInterface; +use MonsieurBiz\SyliusBlogPlugin\Entity\AuthorInterface; +use MonsieurBiz\SyliusBlogPlugin\Entity\TagInterface; +use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; +use Sylius\Component\Channel\Model\ChannelInterface; + +/** + * @template T of ArticleInterface + * + * @implements ArticleRepositoryInterface + */ +final class ArticleRepository extends EntityRepository implements ArticleRepositoryInterface +{ + public function createListQueryBuilder(string $localeCode): QueryBuilder + { + return $this->createQueryBuilder('ba') + ->addSelect('translation') + ->leftJoin('ba.translations', 'translation', 'WITH', 'translation.locale = :localeCode') + ->setParameter('localeCode', $localeCode) + ; + } + + public function createShopListQueryBuilder(string $localeCode, ChannelInterface $channel, ?TagInterface $tag): QueryBuilder + { + $queryBuilder = $this->createListQueryBuilder($localeCode) + ->andWhere(':channel MEMBER OF ba.channels') + ->andWhere('ba.enabled = true') + ->andWhere('ba.state = :state') + ->setParameter('channel', $channel) + ->setParameter('state', ArticleInterface::STATE_PUBLISHED) + ; + + if (null !== $tag) { + $queryBuilder + ->andWhere(':tag MEMBER OF ba.tags') + ->setParameter('tag', $tag) + ; + } + + return $queryBuilder; + } + + public function findAllEnabledAndPublishedByTag(string $localeCode, ChannelInterface $channel, TagInterface $tag, int $limit): array + { + /** @phpstan-ignore-next-line */ + return $this->createShopListQueryBuilder($localeCode, $channel, $tag) + ->setMaxResults($limit) + ->getQuery() + ->getResult() + ; + } + + /** + * @throws NonUniqueResultException + */ + public function findOneBySlug(string $slug, string $localeCode): ?ArticleInterface + { + /** @phpstan-ignore-next-line */ + return $this->createListQueryBuilder($localeCode) + ->andWhere('translation.slug = :slug') + ->setParameter('slug', $slug) + ->getQuery() + ->getOneOrNullResult() + ; + } + + public function findOnePublishedBySlug(string $slug, string $localeCode, ChannelInterface $channel): ?ArticleInterface + { + /** @phpstan-ignore-next-line */ + return $this->createListQueryBuilder($localeCode) + ->andWhere('translation.slug = :slug') + ->andWhere(':channel MEMBER OF ba.channels') + ->andWhere('ba.enabled = true') + ->andWhere('ba.state = :state') + ->setParameter('slug', $slug) + ->setParameter('channel', $channel) + ->setParameter('state', ArticleInterface::STATE_PUBLISHED) + ->getQuery() + ->getOneOrNullResult() + ; + } + + public function findAllEnabledAndPublishedByAuthor(string $localeCode, ChannelInterface $channel, AuthorInterface $author, int $limit): array + { + /** @phpstan-ignore-next-line */ + return $this->createListQueryBuilder($localeCode) + ->andWhere(':channel MEMBER OF ba.channels') + ->andWhere('ba.enabled = true') + ->andWhere('ba.state = :state') + ->andWhere(':author MEMBER OF ba.authors') + ->setParameter('channel', $channel) + ->setParameter('state', ArticleInterface::STATE_PUBLISHED) + ->setParameter('author', $author) + ->addOrderBy('ba.publishedAt', 'DESC') + ->setMaxResults($limit) + ->getQuery() + ->getResult() + ; + } +} diff --git a/src/Repository/ArticleRepositoryInterface.php b/src/Repository/ArticleRepositoryInterface.php new file mode 100644 index 0000000..99ef119 --- /dev/null +++ b/src/Repository/ArticleRepositoryInterface.php @@ -0,0 +1,42 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Repository; + +use Doctrine\ORM\QueryBuilder; +use MonsieurBiz\SyliusBlogPlugin\Entity\ArticleInterface; +use MonsieurBiz\SyliusBlogPlugin\Entity\AuthorInterface; +use MonsieurBiz\SyliusBlogPlugin\Entity\TagInterface; +use Sylius\Component\Channel\Model\ChannelInterface; +use Sylius\Component\Resource\Repository\RepositoryInterface; + +/** + * @template T of ArticleInterface + * + * @extends RepositoryInterface + */ +interface ArticleRepositoryInterface extends RepositoryInterface +{ + public function createListQueryBuilder(string $localeCode): QueryBuilder; + + public function createShopListQueryBuilder(string $localeCode, ChannelInterface $channel, TagInterface $tag): QueryBuilder; + + /** + * @return ArticleInterface[] + */ + public function findAllEnabledAndPublishedByTag(string $localeCode, ChannelInterface $channel, TagInterface $tag, int $limit): array; + + public function findOneBySlug(string $slug, string $localeCode): ?ArticleInterface; + + public function findOnePublishedBySlug(string $slug, string $localeCode, ChannelInterface $channel): ?ArticleInterface; + + public function findAllEnabledAndPublishedByAuthor(string $localeCode, ChannelInterface $channel, AuthorInterface $author, int $limit): array; +} diff --git a/src/Repository/AuthorRepository.php b/src/Repository/AuthorRepository.php new file mode 100644 index 0000000..1ad7a97 --- /dev/null +++ b/src/Repository/AuthorRepository.php @@ -0,0 +1,29 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Repository; + +use Doctrine\ORM\QueryBuilder; +use MonsieurBiz\SyliusBlogPlugin\Entity\AuthorInterface; +use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; + +/** + * @template T of AuthorInterface + * + * @implements AuthorRepositoryInterface + */ +final class AuthorRepository extends EntityRepository implements AuthorRepositoryInterface +{ + public function createListQueryBuilder(): QueryBuilder + { + return $this->createQueryBuilder('a'); + } +} diff --git a/src/Repository/AuthorRepositoryInterface.php b/src/Repository/AuthorRepositoryInterface.php new file mode 100644 index 0000000..32b2b02 --- /dev/null +++ b/src/Repository/AuthorRepositoryInterface.php @@ -0,0 +1,26 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Repository; + +use Doctrine\ORM\QueryBuilder; +use MonsieurBiz\SyliusBlogPlugin\Entity\AuthorInterface; +use Sylius\Component\Resource\Repository\RepositoryInterface; + +/** + * @template T of AuthorInterface + * + * @extends RepositoryInterface + */ +interface AuthorRepositoryInterface extends RepositoryInterface +{ + public function createListQueryBuilder(): QueryBuilder; +} diff --git a/src/Repository/TagRepository.php b/src/Repository/TagRepository.php new file mode 100644 index 0000000..4774114 --- /dev/null +++ b/src/Repository/TagRepository.php @@ -0,0 +1,96 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Repository; + +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\QueryBuilder; +use MonsieurBiz\SyliusBlogPlugin\Entity\ArticleInterface; +use MonsieurBiz\SyliusBlogPlugin\Entity\TagInterface; +use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; + +/** + * @template T of TagInterface + * + * @implements TagRepositoryInterface + */ +final class TagRepository extends EntityRepository implements TagRepositoryInterface +{ + public function findRootNodes(): array + { + /** @phpstan-ignore-next-line */ + return $this->createQueryBuilder('o') + ->addOrderBy('o.position') + ->getQuery() + ->getResult() + ; + } + + public function findHydratedRootNodes(): array + { + $this->createQueryBuilder('o') + ->select(['o', 'ot']) + ->leftJoin('o.translations', 'ot') + ->getQuery() + ->getResult() + ; + + return $this->findRootNodes(); + } + + public function createListQueryBuilder(string $localeCode): QueryBuilder + { + return $this->createQueryBuilder('bc') + ->addSelect('translation') + ->leftJoin('bc.translations', 'translation', 'WITH', 'translation.locale = :localeCode') + ->setParameter('localeCode', $localeCode) + ; + } + + public function createEnabledListQueryBuilder(string $localeCode): QueryBuilder + { + return $this->createListQueryBuilder($localeCode) + ->join('bc.articles', 'articles') + ->andWhere('bc.enabled = true') + ->andWhere('articles.enabled = true') + ->andWhere('articles.state = :state') + ->setParameter('state', ArticleInterface::STATE_PUBLISHED) + ; + } + + /** + * @throws NonUniqueResultException + */ + public function findOneByName(string $name, string $localeCode): ?TagInterface + { + /** @phpstan-ignore-next-line */ + return $this->createListQueryBuilder($localeCode) + ->andWhere('translation.name = :name') + ->setParameter('name', $name) + ->getQuery() + ->getOneOrNullResult() + ; + } + + /** + * @throws NonUniqueResultException + */ + public function findOneBySlug(string $slug, string $localeCode): ?TagInterface + { + /** @phpstan-ignore-next-line */ + return $this->createListQueryBuilder($localeCode) + ->andWhere('translation.slug = :slug') + ->setParameter('slug', $slug) + ->getQuery() + ->getOneOrNullResult() + ; + } +} diff --git a/src/Repository/TagRepositoryInterface.php b/src/Repository/TagRepositoryInterface.php new file mode 100644 index 0000000..143387b --- /dev/null +++ b/src/Repository/TagRepositoryInterface.php @@ -0,0 +1,42 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace MonsieurBiz\SyliusBlogPlugin\Repository; + +use Doctrine\ORM\QueryBuilder; +use MonsieurBiz\SyliusBlogPlugin\Entity\TagInterface; +use Sylius\Component\Resource\Repository\RepositoryInterface; + +/** + * @template T of TagInterface + * + * @extends RepositoryInterface + */ +interface TagRepositoryInterface extends RepositoryInterface +{ + /** + * @return TagInterface[] + */ + public function findRootNodes(): array; + + /** + * @return TagInterface[] + */ + public function findHydratedRootNodes(): array; + + public function createListQueryBuilder(string $localeCode): QueryBuilder; + + public function createEnabledListQueryBuilder(string $localeCode): QueryBuilder; + + public function findOneByName(string $name, string $localeCode): ?TagInterface; + + public function findOneBySlug(string $slug, string $localeCode): ?TagInterface; +} diff --git a/src/Resources/config/config.yaml b/src/Resources/config/config.yaml index e69de29..577647c 100644 --- a/src/Resources/config/config.yaml +++ b/src/Resources/config/config.yaml @@ -0,0 +1,4 @@ +imports: + - { resource: 'resources.yaml' } + - { resource: 'sylius/*.yaml' } + - { resource: 'images.yaml' } diff --git a/src/Resources/config/doctrine/Article.orm.xml b/src/Resources/config/doctrine/Article.orm.xml new file mode 100644 index 0000000..4281142 --- /dev/null +++ b/src/Resources/config/doctrine/Article.orm.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Resources/config/doctrine/ArticleTranslation.orm.xml b/src/Resources/config/doctrine/ArticleTranslation.orm.xml new file mode 100644 index 0000000..4d0c4a3 --- /dev/null +++ b/src/Resources/config/doctrine/ArticleTranslation.orm.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Resources/config/doctrine/Author.orm.xml b/src/Resources/config/doctrine/Author.orm.xml new file mode 100644 index 0000000..4454943 --- /dev/null +++ b/src/Resources/config/doctrine/Author.orm.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/src/Resources/config/doctrine/Tag.orm.xml b/src/Resources/config/doctrine/Tag.orm.xml new file mode 100644 index 0000000..8e134bd --- /dev/null +++ b/src/Resources/config/doctrine/Tag.orm.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Resources/config/doctrine/TagTranslation.orm.xml b/src/Resources/config/doctrine/TagTranslation.orm.xml new file mode 100644 index 0000000..b60f796 --- /dev/null +++ b/src/Resources/config/doctrine/TagTranslation.orm.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Resources/config/images.yaml b/src/Resources/config/images.yaml new file mode 100644 index 0000000..e6ba989 --- /dev/null +++ b/src/Resources/config/images.yaml @@ -0,0 +1,8 @@ +liip_imagine: + filter_sets: + monsieurbiz_blog_image_large_thumbnail: + filters: + thumbnail: { size: [800, 800], mode: outbound } + monsieurbiz_blog_image_thumbnail: + filters: + thumbnail: { size: [260, 260], mode: outbound } diff --git a/src/Resources/config/resources.yaml b/src/Resources/config/resources.yaml new file mode 100644 index 0000000..5230bb0 --- /dev/null +++ b/src/Resources/config/resources.yaml @@ -0,0 +1,30 @@ +sylius_resource: + resources: + monsieurbiz_blog.tag: + classes: + model: MonsieurBiz\SyliusBlogPlugin\Entity\Tag + interface: MonsieurBiz\SyliusBlogPlugin\Entity\TagInterface + repository: MonsieurBiz\SyliusBlogPlugin\Repository\TagRepository + form: MonsieurBiz\SyliusBlogPlugin\Form\Type\TagType + translation: + classes: + model: MonsieurBiz\SyliusBlogPlugin\Entity\TagTranslation + interface: MonsieurBiz\SyliusBlogPlugin\Entity\TagTranslationInterface + form: MonsieurBiz\SyliusBlogPlugin\Form\Type\TagTranslationType + monsieurbiz_blog.article: + classes: + model: MonsieurBiz\SyliusBlogPlugin\Entity\Article + interface: MonsieurBiz\SyliusBlogPlugin\Entity\ArticleInterface + repository: MonsieurBiz\SyliusBlogPlugin\Repository\ArticleRepository + form: MonsieurBiz\SyliusBlogPlugin\Form\Type\ArticleType + translation: + classes: + model: MonsieurBiz\SyliusBlogPlugin\Entity\ArticleTranslation + interface: MonsieurBiz\SyliusBlogPlugin\Entity\ArticleTranslationInterface + form: MonsieurBiz\SyliusBlogPlugin\Form\Type\ArticleTranslationType + monsieurbiz_blog.author: + classes: + model: MonsieurBiz\SyliusBlogPlugin\Entity\Author + interface: MonsieurBiz\SyliusBlogPlugin\Entity\AuthorInterface + repository: MonsieurBiz\SyliusBlogPlugin\Repository\AuthorRepository + form: MonsieurBiz\SyliusBlogPlugin\Form\Type\AuthorType diff --git a/src/Resources/config/routes.yaml b/src/Resources/config/routes.yaml index e69de29..895fdda 100644 --- a/src/Resources/config/routes.yaml +++ b/src/Resources/config/routes.yaml @@ -0,0 +1,9 @@ +monsieurbiz_blog_admin: + resource: "@MonsieurBizSyliusBlogPlugin/Resources/config/routes/admin.yaml" + prefix: /%sylius_admin.path_name% + +monsieurbiz_blog_shop: + resource: "@MonsieurBizSyliusBlogPlugin/Resources/config/routes/shop.yaml" + prefix: /{_locale} + requirements: + _locale: ^[A-Za-z]{2,4}(_([A-Za-z]{4}|[0-9]{3}))?(_([A-Za-z]{2}|[0-9]{3}))?$ diff --git a/src/Resources/config/routes/admin.yaml b/src/Resources/config/routes/admin.yaml new file mode 100644 index 0000000..293ecdf --- /dev/null +++ b/src/Resources/config/routes/admin.yaml @@ -0,0 +1,99 @@ +monsieurbiz_blog_admin_tag: + resource: | + section: admin + alias: monsieurbiz_blog.tag + path: /blog/tag + templates: "@MonsieurBizSyliusBlogPlugin/Admin/Tag" + redirect: update + only: ['update', 'delete'] + vars: + all: + subheader: monsieurbiz_blog.ui.manage_your_blog_tags + index: + route: + name: monsieurbiz_blog_admin_tag_create + type: sylius.resource + +monsieurbiz_blog_admin_tag_index: + path: /blog/tags + controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction + defaults: + route: monsieurbiz_blog_admin_tag_create + permanent: true + keepQueryParams: true + +monsieurbiz_blog_admin_tag_create: + path: /blog/tag/new + methods: [ GET, POST ] + defaults: + _controller: monsieurbiz_blog.controller.tag::createAction + _sylius: + section: admin + permission: true + template: "@MonsieurBizSyliusBlogPlugin/Admin/Tag/create.html.twig" + factory: + method: createNew + redirect: + route: monsieurbiz_blog_admin_tag_create + vars: + subheader: monsieurbiz_blog.ui.manage_your_blog_tags + index: + route: + name: 'monsieurbiz_blog_admin_tag_create' + +monsieurbiz_blog_admin_partial_tags_tree: + path: /_partial/blog/tags/tree + methods: [GET] + defaults: + _controller: monsieurbiz_blog.controller.tag::indexAction + _sylius: + template: $template + repository: + method: findHydratedRootNodes + permission: true + +monsieurbiz_blog_admin_article: + resource: | + section: admin + alias: monsieurbiz_blog.article + path: /blog/articles + templates: "@SyliusAdmin/Crud" + redirect: index + except: ['show'] + grid: monsieurbiz_blog_admin_article + vars: + index: + icon: 'newspaper' + update: + templates: + toolbar: "@MonsieurBizSyliusBlogPlugin/Admin/Article/Update/_toolbar.html.twig" + type: sylius.resource + +monsieurbiz_blog_admin_article_update_state: + path: /blog/articles/{id}/{state} + methods: [PUT] + defaults: + _controller: monsieurbiz_blog.controller.article::applyStateMachineTransitionAction + _sylius: + event: $state + section: admin + permission: true + state_machine: + graph: !php/const \MonsieurBiz\SyliusBlogPlugin\Entity\ArticleInterface::GRAPH + transition: $state + redirect: referer + flash: monsieurbiz_blog.blog.article.$state + +monsieurbiz_blog_admin_author: + resource: | + section: admin + alias: monsieurbiz_blog.author + path: /blog/authors + templates: "@SyliusAdmin/Crud" + redirect: index + except: ['show'] + grid: monsieurbiz_blog_admin_author + vars: + index: + icon: 'user' + type: sylius.resource diff --git a/src/Resources/config/routes/shop.yaml b/src/Resources/config/routes/shop.yaml new file mode 100644 index 0000000..c7fb80f --- /dev/null +++ b/src/Resources/config/routes/shop.yaml @@ -0,0 +1,60 @@ +monsieurbiz_blog_index: + path: /blog + methods: [ GET ] + defaults: + _controller: monsieurbiz_blog.controller.article::indexAction + _sylius: + template: "@MonsieurBizSyliusBlogPlugin/Shop/Article/index.html.twig" + grid: monsieurbiz_blog_shop_article + +monsieurbiz_blog_tag_show: + path: /blog/tag/{slug} + methods: [ GET ] + defaults: + _controller: monsieurbiz_blog.controller.article::indexAction + _sylius: + template: "@MonsieurBizSyliusBlogPlugin/Shop/Article/index.html.twig" + grid: monsieurbiz_blog_shop_tag_article + parameters: + tag: "expr:notFoundOnNull(service('monsieurbiz_blog.repository.tag').findOneBySlug($slug, service('sylius.context.locale').getLocaleCode()))" + +monsieurbiz_blog_partial_tag_show: + path: /_partial/blog/tag/{slug} + methods: [GET] + defaults: + _controller: monsieurbiz_blog.controller.article::indexAction + _sylius: + template: "@MonsieurBizSyliusBlogPlugin/Shop/Article/_list.html.twig" + grid: monsieurbiz_blog_shop_article + paginate: false + requirements: + slug: "[^/]+" + +monsieurbiz_blog_partial_tag_show_by_slug: + path: /_partial/blog/tag/by-slug/{slug} + methods: [GET] + defaults: + _controller: monsieurbiz_blog.controller.tag::showAction + _sylius: + template: $template + repository: + method: findOneBySlug + arguments: + - $slug + - "expr:service('sylius.context.locale').getLocaleCode()" + requirements: + slug: .+ + +monsieurbiz_blog_article_show: + path: /blog/{slug} + methods: [ GET ] + defaults: + _controller: monsieurbiz_blog.controller.article::showAction + _sylius: + template: "@MonsieurBizSyliusBlogPlugin/Shop/Article/show.html.twig" + repository: + method: findOnePublishedBySlug + arguments: + - $slug + - "expr:service('sylius.context.locale').getLocaleCode()" + - "expr:service('sylius.context.channel').getChannel()" diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index 3847bdb..2e23217 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -8,7 +8,42 @@ services: resource: '../../*' exclude: '../../{Entity,Migrations}' - MonsieurBiz\SyliusBlogPlugin\Controller\: - resource: '../../Controller' - tags: [ 'controller.service_arguments' ] + # Add menu item in admin panel + MonsieurBiz\SyliusBlogPlugin\EventListener\AdminMenuListener: + tags: + - { name: kernel.event_listener, event: sylius.menu.admin.main } + # Form types + MonsieurBiz\SyliusBlogPlugin\Form\Type\TagType: + arguments: + $dataClass: '%monsieurbiz_blog.model.tag.class%' + $validationGroups: ['monsieurbiz'] + MonsieurBiz\SyliusBlogPlugin\Form\Type\TagTranslationType: + arguments: + $dataClass: '%monsieurbiz_blog.model.tag_translation.class%' + $validationGroups: ['monsieurbiz'] + + MonsieurBiz\SyliusBlogPlugin\Form\Type\ArticleType: + arguments: + $dataClass: '%monsieurbiz_blog.model.article.class%' + $validationGroups: ['monsieurbiz'] + + MonsieurBiz\SyliusBlogPlugin\Form\Type\ArticleTranslationType: + arguments: + $dataClass: '%monsieurbiz_blog.model.article_translation.class%' + $validationGroups: ['monsieurbiz'] + + MonsieurBiz\SyliusBlogPlugin\Form\Type\AuthorType: + arguments: + $dataClass: '%monsieurbiz_blog.model.author.class%' + $validationGroups: [ 'monsieurbiz' ] + + # Menus + MonsieurBiz\SyliusBlogPlugin\Menu\AdminArticleUpdateMenuBuilder: + tags: + - { name: knp_menu.menu_builder, method: createMenu, alias: monsieurbiz_blog.admin.article.update } + + # Fixtures + MonsieurBiz\SyliusBlogPlugin\Fixture\Factory\ArticleFixtureFactory: + arguments: + $fileLocator: '@file_locator' diff --git a/src/Resources/config/sylius/fixtures.yaml b/src/Resources/config/sylius/fixtures.yaml new file mode 100644 index 0000000..74c5764 --- /dev/null +++ b/src/Resources/config/sylius/fixtures.yaml @@ -0,0 +1,15 @@ +sylius_fixtures: + suites: + default: + fixtures: + monsieurbiz_blog_tag: + options: + random: 30 + + monsieubiz_blog_author: + options: + random: 3 + + monsieubiz_blog_article: + options: + random: 100 diff --git a/src/Resources/config/sylius/grids.yaml b/src/Resources/config/sylius/grids.yaml new file mode 100644 index 0000000..9a9bbf2 --- /dev/null +++ b/src/Resources/config/sylius/grids.yaml @@ -0,0 +1,2 @@ +imports: + - { resource: 'grids/*.yaml' } diff --git a/src/Resources/config/sylius/grids/monsieurbiz_blog_admin_article.yaml b/src/Resources/config/sylius/grids/monsieurbiz_blog_admin_article.yaml new file mode 100644 index 0000000..ff62345 --- /dev/null +++ b/src/Resources/config/sylius/grids/monsieurbiz_blog_admin_article.yaml @@ -0,0 +1,102 @@ +sylius_grid: + grids: + monsieurbiz_blog_admin_article: + driver: + name: doctrine/orm + options: + class: '%monsieurbiz_blog.model.article.class%' + repository: + method: createListQueryBuilder + arguments: [ "expr:service('sylius.context.locale').getLocaleCode()" ] + sorting: + id: desc + filters: + title: + type: string + label: monsieurbiz_blog.ui.title + options: + fields: [ translation.title ] + enabled: + type: boolean + label: sylius.ui.enabled + state: + type: select + label: sylius.ui.state + form_options: + choices: + monsieurbiz_blog.ui.draft: draft + monsieurbiz_blog.ui.published: published + channel: + type: entity + label: sylius.ui.channel + options: + fields: [channels.id] + form_options: + class: "%sylius.model.channel.class%" + publishedAt: + type: date + label: monsieurbiz_blog.ui.published_at + fields: + id: + label: sylius.ui.id + type: string + sortable: ~ + title: + type: string + label: monsieurbiz_blog.ui.title + authors: + type: twig + label: monsieurbiz_blog.form.article.authors + options: + template: '@MonsieurBizSyliusBlogPlugin/Admin/Grid/Field/_authors.html.twig' + channels: + type: twig + label: sylius.ui.channels + options: + template: '@SyliusAdmin/Grid/Field/_channels.html.twig' + enabled: + type: twig + label: sylius.ui.enabled + options: + template: "@SyliusUi/Grid/Field/enabled.html.twig" + state: + type: twig + label: sylius.ui.state + sortable: ~ + options: + template: "@SyliusUi/Grid/Field/state.html.twig" + vars: + labels: "@MonsieurBizSyliusBlogPlugin/Admin/Article/State" + updatedAt: + type: datetime + sortable: ~ + label: sylius.ui.updating_date + publishedAt: + type: datetime + sortable: ~ + label: monsieurbiz_blog.ui.published_at + actions: + main: + create: + type: create + item: + update: + type: update + delete: + type: delete + publish: + type: apply_transition + label: monsieurbiz_blog.ui.publish + icon: check + options: + link: + route: monsieurbiz_blog_admin_article_update_state + parameters: + id: resource.id + state: !php/const \MonsieurBiz\SyliusBlogPlugin\Entity\ArticleInterface::TRANSITION_PUBLISH + class: green + transition: !php/const \MonsieurBiz\SyliusBlogPlugin\Entity\ArticleInterface::TRANSITION_PUBLISH + graph: !php/const \MonsieurBiz\SyliusBlogPlugin\Entity\ArticleInterface::GRAPH + bulk: + delete: + type: delete diff --git a/src/Resources/config/sylius/grids/monsieurbiz_blog_admin_author.yaml b/src/Resources/config/sylius/grids/monsieurbiz_blog_admin_author.yaml new file mode 100644 index 0000000..22457e4 --- /dev/null +++ b/src/Resources/config/sylius/grids/monsieurbiz_blog_admin_author.yaml @@ -0,0 +1,35 @@ +sylius_grid: + grids: + monsieurbiz_blog_admin_author: + driver: + name: doctrine/orm + options: + class: '%monsieurbiz_blog.model.author.class%' + repository: + method: createListQueryBuilder + sorting: + id: desc + filters: + name: + type: string + label: monsieurbiz_blog.ui.name + fields: + id: + label: sylius.ui.id + type: string + sortable: ~ + name: + type: string + label: monsieurbiz_blog.ui.name + actions: + main: + create: + type: create + item: + update: + type: update + delete: + type: delete + bulk: + delete: + type: delete diff --git a/src/Resources/config/sylius/grids/monsieurbiz_blog_shop_article.yaml b/src/Resources/config/sylius/grids/monsieurbiz_blog_shop_article.yaml new file mode 100644 index 0000000..e6f4ff3 --- /dev/null +++ b/src/Resources/config/sylius/grids/monsieurbiz_blog_shop_article.yaml @@ -0,0 +1,50 @@ +sylius_grid: + grids: + monsieurbiz_blog_shop_article: + driver: + name: doctrine/orm + options: + class: '%monsieurbiz_blog.model.article.class%' + repository: + method: createShopListQueryBuilder + arguments: + locale: "expr:service('sylius.context.locale').getLocaleCode()" + channel: "expr:service('sylius.context.channel').getChannel()" + tag: null + sorting: + publishedAt: desc + limits: [9] + fields: + id: + label: sylius.ui.id + type: string + sortable: ~ + title: + type: string + label: monsieurbiz_blog.ui.title + channels: + type: twig + label: sylius.ui.channels + options: + template: '@SyliusAdmin/Grid/Field/_channels.html.twig' + enabled: + type: twig + label: sylius.ui.enabled + options: + template: "@SyliusUi/Grid/Field/enabled.html.twig" + state: + type: twig + label: sylius.ui.state + sortable: ~ + options: + template: "@SyliusUi/Grid/Field/state.html.twig" + vars: + labels: "@MonsieurBizSyliusBlogPlugin/Admin/Article/State" + updatedAt: + type: datetime + sortable: ~ + label: sylius.ui.updating_date + publishedAt: + type: datetime + sortable: ~ + label: monsieurbiz_blog.ui.published_at diff --git a/src/Resources/config/sylius/grids/monsieurbiz_blog_shop_category.yaml b/src/Resources/config/sylius/grids/monsieurbiz_blog_shop_category.yaml new file mode 100644 index 0000000..286d1c9 --- /dev/null +++ b/src/Resources/config/sylius/grids/monsieurbiz_blog_shop_category.yaml @@ -0,0 +1,27 @@ +sylius_grid: + grids: + monsieurbiz_blog_shop_tag: + driver: + name: doctrine/orm + options: + class: '%monsieurbiz_blog.model.tag.class%' + repository: + method: createEnabledListQueryBuilder + arguments: [ "expr:service('sylius.context.locale').getLocaleCode()" ] + sorting: + position: asc + fields: + id: + label: sylius.ui.id + type: string + sortable: ~ + name: + type: string + position: + type: string + sortable: ~ + enabled: + type: twig + label: sylius.ui.enabled + options: + template: "@SyliusUi/Grid/Field/enabled.html.twig" diff --git a/src/Resources/config/sylius/grids/monsieurbiz_blog_shop_tag_article.yaml b/src/Resources/config/sylius/grids/monsieurbiz_blog_shop_tag_article.yaml new file mode 100644 index 0000000..690bf02 --- /dev/null +++ b/src/Resources/config/sylius/grids/monsieurbiz_blog_shop_tag_article.yaml @@ -0,0 +1,50 @@ +sylius_grid: + grids: + monsieurbiz_blog_shop_tag_article: + driver: + name: doctrine/orm + options: + class: '%monsieurbiz_blog.model.article.class%' + repository: + method: createShopListQueryBuilder + arguments: + locale: "expr:service('sylius.context.locale').getLocaleCode()" + channel: "expr:service('sylius.context.channel').getChannel()" + tag: "expr:notFoundOnNull(service('monsieurbiz_blog.repository.tag').findOneBySlug($slug, service('sylius.context.locale').getLocaleCode()))" + sorting: + publishedAt: desc + limits: [9] + fields: + id: + label: sylius.ui.id + type: string + sortable: ~ + title: + type: string + label: monsieurbiz_blog.ui.title + channels: + type: twig + label: sylius.ui.channels + options: + template: '@SyliusAdmin/Grid/Field/_channels.html.twig' + enabled: + type: twig + label: sylius.ui.enabled + options: + template: "@SyliusUi/Grid/Field/enabled.html.twig" + state: + type: twig + label: sylius.ui.state + sortable: ~ + options: + template: "@SyliusUi/Grid/Field/state.html.twig" + vars: + labels: "@MonsieurBizSyliusBlogPlugin/Admin/Article/State" + updatedAt: + type: datetime + sortable: ~ + label: sylius.ui.updating_date + publishedAt: + type: datetime + sortable: ~ + label: monsieurbiz_blog.ui.published_at diff --git a/src/Resources/config/sylius/state_machine.yaml b/src/Resources/config/sylius/state_machine.yaml new file mode 100644 index 0000000..f07b8a3 --- /dev/null +++ b/src/Resources/config/sylius/state_machine.yaml @@ -0,0 +1,2 @@ +imports: + - { resource: 'state_machine/*.yaml' } diff --git a/src/Resources/config/sylius/state_machine/monsieurbiz_blog_article.yaml b/src/Resources/config/sylius/state_machine/monsieurbiz_blog_article.yaml new file mode 100644 index 0000000..4cf4d75 --- /dev/null +++ b/src/Resources/config/sylius/state_machine/monsieurbiz_blog_article.yaml @@ -0,0 +1,20 @@ +winzou_state_machine: + monsieurbiz_blog_article: + class: "%monsieurbiz_blog.model.article.class%" + property_path: state + graph: monsieurbiz_blog_article + state_machine_class: "%sylius.state_machine.class%" + states: + draft: ~ + published: ~ + transitions: + publish: + from: [draft, ready] + to: published + callbacks: + after: + monsieurbiz_blog_article_save_publish_date: + on: ["publish"] + do: ["object", "publish"] + args: ["object"] + priority: -100 diff --git a/src/Resources/config/sylius/ui.yaml b/src/Resources/config/sylius/ui.yaml new file mode 100644 index 0000000..e825018 --- /dev/null +++ b/src/Resources/config/sylius/ui.yaml @@ -0,0 +1,7 @@ +sylius_ui: + events: + sylius.shop.layout.topbar: + blocks: + currency_switcher: + template: "@MonsieurBizSyliusBlogPlugin/Shop/Layout/Topbar/_blogLink.html.twig" + priority: 15 diff --git a/src/Resources/config/validation/Article.yaml b/src/Resources/config/validation/Article.yaml new file mode 100644 index 0000000..d72a077 --- /dev/null +++ b/src/Resources/config/validation/Article.yaml @@ -0,0 +1,11 @@ +MonsieurBiz\SyliusBlogPlugin\Entity\Article: + properties: + tags: + - NotBlank: + groups: [ monsieurbiz ] + - Count: + min: 1 + groups: [ monsieurbiz ] + translations: + - Valid: + groups: [ monsieurbiz ] diff --git a/src/Resources/config/validation/ArticleTranslation.yaml b/src/Resources/config/validation/ArticleTranslation.yaml new file mode 100644 index 0000000..262c248 --- /dev/null +++ b/src/Resources/config/validation/ArticleTranslation.yaml @@ -0,0 +1,27 @@ +MonsieurBiz\SyliusBlogPlugin\Entity\ArticleTranslation: + constraints: + - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: + fields: [ slug, locale ] + errorPath: slug + groups: [ monsieurbiz ] + properties: + title: + - NotBlank: + groups: [ monsieurbiz ] + - Length: + min: 2 + max: 255 + groups: [ monsieurbiz ] + slug: + - NotBlank: + groups: [ monsieurbiz ] + - Length: + min: 2 + max: 255 + groups: [ monsieurbiz ] + description: + - NotBlank: + groups: [ monsieurbiz ] + content: + - NotBlank: + groups: [ monsieurbiz ] diff --git a/src/Resources/config/validation/Author.yaml b/src/Resources/config/validation/Author.yaml new file mode 100644 index 0000000..1251a30 --- /dev/null +++ b/src/Resources/config/validation/Author.yaml @@ -0,0 +1,9 @@ +MonsieurBiz\SyliusBlogPlugin\Entity\Author: + properties: + name: + - NotBlank: + groups: [ monsieurbiz ] + - Length: + min: 2 + max: 255 + groups: [ monsieurbiz ] diff --git a/src/Resources/config/validation/Tag.yaml b/src/Resources/config/validation/Tag.yaml new file mode 100644 index 0000000..d3a3293 --- /dev/null +++ b/src/Resources/config/validation/Tag.yaml @@ -0,0 +1,5 @@ +MonsieurBiz\SyliusBlogPlugin\Entity\Tag: + properties: + translations: + - Valid: + groups: [ monsieurbiz ] diff --git a/src/Resources/config/validation/TagTranslation.yaml b/src/Resources/config/validation/TagTranslation.yaml new file mode 100644 index 0000000..1a2a8e8 --- /dev/null +++ b/src/Resources/config/validation/TagTranslation.yaml @@ -0,0 +1,21 @@ +MonsieurBiz\SyliusBlogPlugin\Entity\TagTranslation: + constraints: + - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: + fields: [ slug, locale ] + errorPath: slug + groups: [ monsieurbiz ] + properties: + name: + - NotBlank: + groups: [ monsieurbiz ] + - Length: + min: 2 + max: 255 + groups: [ monsieurbiz ] + slug: + - NotBlank: + groups: [ monsieurbiz ] + - Length: + min: 2 + max: 255 + groups: [ monsieurbiz ] diff --git a/src/Resources/fixtures/article-1.jpg b/src/Resources/fixtures/article-1.jpg new file mode 100644 index 0000000..02d640e Binary files /dev/null and b/src/Resources/fixtures/article-1.jpg differ diff --git a/src/Resources/fixtures/article-2.jpg b/src/Resources/fixtures/article-2.jpg new file mode 100644 index 0000000..6451e1a Binary files /dev/null and b/src/Resources/fixtures/article-2.jpg differ diff --git a/src/Resources/fixtures/article-3.jpg b/src/Resources/fixtures/article-3.jpg new file mode 100644 index 0000000..2e960ee Binary files /dev/null and b/src/Resources/fixtures/article-3.jpg differ diff --git a/src/Resources/fixtures/article-4.jpg b/src/Resources/fixtures/article-4.jpg new file mode 100644 index 0000000..fb55685 Binary files /dev/null and b/src/Resources/fixtures/article-4.jpg differ diff --git a/src/Resources/fixtures/article-5.jpg b/src/Resources/fixtures/article-5.jpg new file mode 100644 index 0000000..5a6be7a Binary files /dev/null and b/src/Resources/fixtures/article-5.jpg differ diff --git a/src/Resources/translations/messages.en.yaml b/src/Resources/translations/messages.en.yaml new file mode 100644 index 0000000..db7a095 --- /dev/null +++ b/src/Resources/translations/messages.en.yaml @@ -0,0 +1,36 @@ +monsieurbiz_blog: + form: + tag: + title: Title + slug: Slug + article: + tags: Tags + title: Title + slug: Slug + content: Content + image: Image + description: Description + authors: Authors + author: + name: Name + ui: + menu_blog: Blog + tags: Tags + new_tag: New tag + edit_tag: Edit tag + manage_your_blog_tags: Manage your blog tags + articles: Articles + new_article: New article + edit_article: Edit article + title: Title + published_at: Published at + publish: Publish + draft: Draft + published: Published + see_more_articles: See more articles + authors: Authors + new_author: New author + edit_author: Edit author + name: Name + blog: Blog + read_more: Read more diff --git a/src/Resources/translations/messages.fr.yaml b/src/Resources/translations/messages.fr.yaml new file mode 100644 index 0000000..4e17a3e --- /dev/null +++ b/src/Resources/translations/messages.fr.yaml @@ -0,0 +1,36 @@ +monsieurbiz_blog: + form: + tag: + title: Titre + slug: Slug + article: + tags: Tags + title: Titre + slug: Slug + content: Contenu + image: Image + description: Description + authors: Auteurs + author: + name: Nom + ui: + menu_blog: Blog + tags: Tags + new_tag: Nouveau tag + edit_tag: Modifier le tag + manage_your_blog_tags: Gérer vos tags de blog + articles: Articles + new_article: Nouvel article + edit_article: Modifier l'article + title: Titre + published_at: Publié le + publish: Publier + draft: Brouillon + published: Publié + see_more_articles: Voir plus d'articles + authors: Auteurs + new_author: Nouvel auteur + edit_author: Modifier l'auteur + name: Nom + blog: Blog + read_more: Lire la suite diff --git a/src/Resources/views/.gitignore b/src/Resources/views/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/src/Resources/views/Admin/Article/State/draft.html.twig b/src/Resources/views/Admin/Article/State/draft.html.twig new file mode 100644 index 0000000..599d9e5 --- /dev/null +++ b/src/Resources/views/Admin/Article/State/draft.html.twig @@ -0,0 +1,4 @@ + + + {{ value|replace({'sylius.ui': 'monsieurbiz_blog.ui'})|trans }} + diff --git a/src/Resources/views/Admin/Article/State/published.html.twig b/src/Resources/views/Admin/Article/State/published.html.twig new file mode 100644 index 0000000..751eb1d --- /dev/null +++ b/src/Resources/views/Admin/Article/State/published.html.twig @@ -0,0 +1,4 @@ + + + {{ value|replace({'sylius.ui': 'monsieurbiz_blog.ui'})|trans }} + diff --git a/src/Resources/views/Admin/Article/Update/_toolbar.html.twig b/src/Resources/views/Admin/Article/Update/_toolbar.html.twig new file mode 100644 index 0000000..7fd3646 --- /dev/null +++ b/src/Resources/views/Admin/Article/Update/_toolbar.html.twig @@ -0,0 +1,4 @@ +
+ {% set menu = knp_menu_get('monsieurbiz_blog.admin.article.update', [], {'article': article}) %} + {{ knp_menu_render(menu, {'template': '@SyliusUi/Menu/top.html.twig'}) }} +
diff --git a/src/Resources/views/Admin/Grid/Field/_authors.html.twig b/src/Resources/views/Admin/Grid/Field/_authors.html.twig new file mode 100644 index 0000000..44f2318 --- /dev/null +++ b/src/Resources/views/Admin/Grid/Field/_authors.html.twig @@ -0,0 +1,3 @@ +{% for author in data %} + {{ author.name }}{% if not loop.last %},{% endif %} +{% endfor %} diff --git a/src/Resources/views/Admin/Tag/_tree.html.twig b/src/Resources/views/Admin/Tag/_tree.html.twig new file mode 100644 index 0000000..0368e82 --- /dev/null +++ b/src/Resources/views/Admin/Tag/_tree.html.twig @@ -0,0 +1,54 @@ +{% import '@SyliusUi/Macro/buttons.html.twig' as buttons %} +{% import _self as tree %} + +{% macro render(items) %} + {% import _self as tree %} + +
    + {% for item in items %} + {% if item.id is not null %} +
  • +
    +
    + +
    + +
    + +
    +
    +
  • + {% endif %} + {% endfor %} +
+{% endmacro %} + + diff --git a/src/Resources/views/Admin/Tag/create.html.twig b/src/Resources/views/Admin/Tag/create.html.twig new file mode 100644 index 0000000..0fc5ce4 --- /dev/null +++ b/src/Resources/views/Admin/Tag/create.html.twig @@ -0,0 +1,12 @@ +{% extends '@SyliusAdmin/Crud/create.html.twig' %} + +{% block content %} +
+
+ {{ render(path('monsieurbiz_blog_admin_partial_tags_tree', {'template': '@MonsieurBizSyliusBlogPlugin/Admin/Tag/_tree.html.twig'})) }} +
+
+ {{ parent() }} +
+
+{% endblock %} diff --git a/src/Resources/views/Admin/Tag/update.html.twig b/src/Resources/views/Admin/Tag/update.html.twig new file mode 100644 index 0000000..614e50e --- /dev/null +++ b/src/Resources/views/Admin/Tag/update.html.twig @@ -0,0 +1,12 @@ +{% extends '@SyliusAdmin/Crud/update.html.twig' %} + +{% block content %} +
+
+ {{ render(path('monsieurbiz_blog_admin_partial_tags_tree', {'template': '@MonsieurBizSyliusBlogPlugin/Admin/Tag/_tree.html.twig'})) }} +
+
+ {{ parent() }} +
+
+{% endblock %} diff --git a/src/Resources/views/Shop/Article/_image.html.twig b/src/Resources/views/Shop/Article/_image.html.twig new file mode 100644 index 0000000..3fe4eec --- /dev/null +++ b/src/Resources/views/Shop/Article/_image.html.twig @@ -0,0 +1,14 @@ +{% set filter = filter|default('monsieurbiz_blog_image_thumbnail') %} +{% set placeholder = placeholder|default('200x200.png') %} + +{% if article.image %} + {% set path = article.image|imagine_filter(filter) %} +{% else %} + {% if use_webpack %} + {% set path = asset('build/shop/images/' ~ placeholder, 'shop') %} + {% else %} + {% set path = asset('assets/shop/img/' ~ placeholder) %} + {% endif %} +{% endif %} + +{{ article.title }} diff --git a/src/Resources/views/Shop/Article/_list.html.twig b/src/Resources/views/Shop/Article/_list.html.twig new file mode 100644 index 0000000..6b154c4 --- /dev/null +++ b/src/Resources/views/Shop/Article/_list.html.twig @@ -0,0 +1,62 @@ +{% import '@SyliusUi/Macro/messages.html.twig' as messages %} +{% import '@SyliusUi/Macro/pagination.html.twig' as pagination %} + +{% if resources.data|length %} +
+ {% for article in articles.data %} + {% if article.image %} + {% set path = article.image|imagine_filter('monsieurbiz_blog_image_thumbnail') %} + {% else %} + {% if use_webpack %} + {% set path = asset('build/shop/images/200x200.png', 'shop') %} + {% else %} + {% set path = asset('assets/shop/img/200x200.png') %} + {% endif %} + {% endif %} + +
+ + + + {% include '@MonsieurBizSyliusBlogPlugin/Shop/Article/_image.html.twig' %} + +
+ + {{ article.title }} + +
+ {{ article.publishedAt|format_date() }} +
+ {% if article.description %} +
+ {{ article.description|nl2br }} +
+ {% endif %} +
+ + {% set authors = article.authors|default([]) %} + {% if authors|length %} +
+
+ + {{ authors|map(author => author.name)|join(', ') }} +
+
+ {% endif %} +
+ {% endfor %} +
+ {% if configuration.isPaginated %} + {{ pagination.simple(resources.data) }} + {% endif %} +{% else %} + {{ messages.info('sylius.ui.no_results_to_display') }} +{% endif %} diff --git a/src/Resources/views/Shop/Article/_structured_data.html.twig b/src/Resources/views/Shop/Article/_structured_data.html.twig new file mode 100644 index 0000000..f38155a --- /dev/null +++ b/src/Resources/views/Shop/Article/_structured_data.html.twig @@ -0,0 +1,32 @@ +{% set newsArticle = { + "@context": "https://schema.org", + "@type": "NewsArticle", + "headline": article.title, + "datePublished": article.publishedAt|date('c'), + "dateModified": article.updatedAt|date('c'), +} %} + +{% if article.image is not empty %} + {% set newsArticle = newsArticle|merge({ + "image": [ + article.image|default('monsieurbiz_blog_image_thumbnail'), + ] + }) %} +{% endif %} + +{% if article.authors is not empty %} + {% set authors = [] %} + {% for author in article.authors %} + {% set authors = authors|merge([{ + "@type": "Person", + "name": author.name, + }]) %} + {% endfor %} + {% set newsArticle = newsArticle|merge({ + "author": authors, + }) %} +{% endif %} + + diff --git a/src/Resources/views/Shop/Article/index.html.twig b/src/Resources/views/Shop/Article/index.html.twig new file mode 100644 index 0000000..2cdd7d1 --- /dev/null +++ b/src/Resources/views/Shop/Article/index.html.twig @@ -0,0 +1,16 @@ +{% extends '@SyliusShop/layout.html.twig' %} +{% set tag = configuration.parameters.get('parameters')['tag'] ?? null %} + +{% block title %} + {% if tag is defined and tag is not null %} + {{ tag.name }} - {{ 'monsieurbiz_blog.ui.blog'|trans }} {{ parent() }} + {% else %} + {{ 'monsieurbiz_blog.ui.blog'|trans }} {{ parent() }} + {% endif %} +{% endblock %} + +{% block content %} + {% include '@MonsieurBizSyliusBlogPlugin/Shop/Tag/_header.html.twig' with {'tag': tag} %} + + {% include '@MonsieurBizSyliusBlogPlugin/Shop/Article/_list.html.twig' %} +{% endblock %} diff --git a/src/Resources/views/Shop/Article/show.html.twig b/src/Resources/views/Shop/Article/show.html.twig new file mode 100644 index 0000000..f01fa7d --- /dev/null +++ b/src/Resources/views/Shop/Article/show.html.twig @@ -0,0 +1,65 @@ +{% extends '@SyliusShop/layout.html.twig' %} + +{% block title %} + {{ article.title }} - {{ 'monsieurbiz_blog.ui.blog'|trans }} {{ parent() }} +{% endblock %} + +{% block metatags %} + {% include '@MonsieurBizSyliusBlogPlugin/Shop/Article/_structured_data.html.twig' %} +{% endblock %} + +{% block content %} + + +
+
+
+
+
+ {% for tag in article.tags %} + + {{ tag.name }} + + {% endfor %} +
+

+ {{ article.title }} +

+ {% if article.authors|length %} +
+ {% for author in article.authors %} +
+
+
+ {{ author.name }} +
+
+
+ {% endfor %} +
+ {% endif %} +
+ +
+ {% include '@MonsieurBizSyliusBlogPlugin/Shop/Article/_image.html.twig' with {'filter': 'monsieurbiz_blog_image_large_thumbnail'} %} +
+ +
+
+ {{ article.publishedAt|format_date() }} +
+
+ {{ article.content|monsieurbiz_richeditor_render_field }} +
+
+
+
+
+{% endblock %} + diff --git a/src/Resources/views/Shop/Layout/Topbar/_blogLink.html.twig b/src/Resources/views/Shop/Layout/Topbar/_blogLink.html.twig new file mode 100644 index 0000000..53c5aba --- /dev/null +++ b/src/Resources/views/Shop/Layout/Topbar/_blogLink.html.twig @@ -0,0 +1,5 @@ + diff --git a/src/Resources/views/Shop/Tag/_header.html.twig b/src/Resources/views/Shop/Tag/_header.html.twig new file mode 100644 index 0000000..34a5dea --- /dev/null +++ b/src/Resources/views/Shop/Tag/_header.html.twig @@ -0,0 +1,13 @@ + + +

{{ tag.name|default('monsieurbiz_blog.ui.blog'|trans) }}

diff --git a/symfony.lock b/symfony.lock new file mode 100644 index 0000000..41ec294 --- /dev/null +++ b/symfony.lock @@ -0,0 +1,470 @@ +{ + "api-platform/core": { + "version": "2.7", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.5", + "ref": "b86557ce5677fa855b1b2608f4a4bc4a8fed8be7" + }, + "files": [ + "config/packages/api_platform.yaml", + "config/routes/api_platform.yaml", + "src/Entity/.gitignore" + ] + }, + "babdev/pagerfanta-bundle": { + "version": "v3.8.0" + }, + "doctrine/annotations": { + "version": "2.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.10", + "ref": "64d8583af5ea57b7afa4aba4b159907f3a148b05" + } + }, + "doctrine/doctrine-bundle": { + "version": "2.11", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.10", + "ref": "c170ded8fc587d6bd670550c43dafcf093762245" + }, + "files": [ + "config/packages/doctrine.yaml", + "src/Entity/.gitignore", + "src/Repository/.gitignore" + ] + }, + "doctrine/doctrine-migrations-bundle": { + "version": "3.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.1", + "ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33" + }, + "files": [ + "config/packages/doctrine_migrations.yaml", + "migrations/.gitignore" + ] + }, + "friends-of-behat/symfony-extension": { + "version": "2.5", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "2.0", + "ref": "1e012e04f573524ca83795cd19df9ea690adb604" + } + }, + "friendsofphp/php-cs-fixer": { + "version": "3.56", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.0", + "ref": "be2103eb4a20942e28a6dd87736669b757132435" + }, + "files": [ + ".php-cs-fixer.dist.php" + ] + }, + "friendsofsymfony/rest-bundle": { + "version": "3.7", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "3.0", + "ref": "3762cc4e4f2d6faabeca5a151b41c8c791bd96e5" + } + }, + "jms/serializer-bundle": { + "version": "4.2", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "4.0", + "ref": "cc04e10cf7171525b50c18b36004edf64cb478be" + } + }, + "knplabs/knp-gaufrette-bundle": { + "version": "v0.8.0" + }, + "knplabs/knp-menu-bundle": { + "version": "v3.4.2" + }, + "league/flysystem-bundle": { + "version": "2.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "913dc3d7a5a1af0d2b044c5ac3a16e2f851d7380" + }, + "files": [ + "config/packages/flysystem.yaml", + "var/storage/.gitignore" + ] + }, + "lexik/jwt-authentication-bundle": { + "version": "2.17", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.5", + "ref": "e9481b233a11ef7e15fe055a2b21fd3ac1aa2bb7" + }, + "files": [ + "config/packages/lexik_jwt_authentication.yaml" + ] + }, + "liip/imagine-bundle": { + "version": "2.13", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.8", + "ref": "d1227d002b70d1a1f941d91845fcd7ac7fbfc929" + } + }, + "monsieurbiz/sylius-media-manager-plugin": { + "version": "1.1", + "recipe": { + "repo": "github.com/monsieurbiz/symfony-recipes", + "branch": "master", + "version": "1.0", + "ref": "be326a8cd381bf47d4102257a96d66ff38a7fa47" + } + }, + "monsieurbiz/sylius-rich-editor-plugin": { + "version": "2.0", + "recipe": { + "repo": "github.com/monsieurbiz/symfony-recipes", + "branch": "master", + "version": "2.0", + "ref": "6b1d6e236466458d8f8d285eba0724487a305ff0" + } + }, + "nyholm/psr7": { + "version": "1.8", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "4a8c0345442dcca1d8a2c65633dcf0285dd5a5a2" + }, + "files": [ + "config/packages/nyholm_psr7.yaml" + ] + }, + "payum/payum-bundle": { + "version": "2.5", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "2.4", + "ref": "518ac22defa04a8a1d82479ed362e2921487adf0" + } + }, + "phpstan/phpstan": { + "version": "1.11", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.0", + "ref": "5e490cc197fb6bb1ae22e5abbc531ddc633b6767" + } + }, + "phpunit/phpunit": { + "version": "9.6", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "9.6", + "ref": "7364a21d87e658eb363c5020c072ecfdc12e2326" + }, + "files": [ + ".env.test", + "phpunit.xml.dist", + "tests/bootstrap.php" + ] + }, + "sonata-project/block-bundle": { + "version": "5.1", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "4.11", + "ref": "b4edd2a1e6ac1827202f336cac2771cb529de542" + } + }, + "sonata-project/form-extensions": { + "version": "2.4", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.4", + "ref": "9c8a1e8ce2b1f215015ed16652c4ed18eb5867fd" + } + }, + "squizlabs/php_codesniffer": { + "version": "3.10", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "3.6", + "ref": "1019e5c08d4821cb9b77f4891f8e9c31ff20ac6f" + } + }, + "stof/doctrine-extensions-bundle": { + "version": "1.12", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.2", + "ref": "e805aba9eff5372e2d149a9ff56566769e22819d" + } + }, + "sylius-labs/doctrine-migrations-extra-bundle": { + "version": "v0.2.0" + }, + "sylius/calendar": { + "version": "v0.5.0" + }, + "sylius/fixtures-bundle": { + "version": "v1.8.0" + }, + "sylius/grid-bundle": { + "version": "v1.12.1" + }, + "sylius/mailer-bundle": { + "version": "v2.0.0" + }, + "sylius/resource-bundle": { + "version": "1.10", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.9", + "ref": "ee21c1fc90778f4b01c20e72c320cc34f8839c1e" + } + }, + "sylius/theme-bundle": { + "version": "v2.3.0" + }, + "symfony/console": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461" + }, + "files": [ + "bin/console" + ] + }, + "symfony/debug-bundle": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "5aa8aa48234c8eb6dbdd7b3cd5d791485d2cec4b" + }, + "files": [ + "config/packages/debug.yaml" + ] + }, + "symfony/flex": { + "version": "2.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "146251ae39e06a95be0fe3d13c807bcf3938b172" + }, + "files": [ + ".env" + ] + }, + "symfony/framework-bundle": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.4", + "ref": "a91c965766ad3ff2ae15981801643330eb42b6a5" + }, + "files": [ + "config/packages/cache.yaml", + "config/packages/framework.yaml", + "config/preload.php", + "config/routes/framework.yaml", + "config/services.yaml", + "public/index.php", + "src/Controller/.gitignore", + "src/Kernel.php" + ] + }, + "symfony/mailer": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "4.3", + "ref": "df66ee1f226c46f01e85c29c2f7acce0596ba35a" + }, + "files": [ + "config/packages/mailer.yaml" + ] + }, + "symfony/messenger": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.0", + "ref": "ba1ac4e919baba5644d31b57a3284d6ba12d52ee" + }, + "files": [ + "config/packages/messenger.yaml" + ] + }, + "symfony/monolog-bundle": { + "version": "3.10", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.7", + "ref": "aff23899c4440dd995907613c1dd709b6f59503f" + }, + "files": [ + "config/packages/monolog.yaml" + ] + }, + "symfony/routing": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.2", + "ref": "e0a11b4ccb8c9e70b574ff5ad3dfdcd41dec5aa6" + }, + "files": [ + "config/packages/routing.yaml", + "config/routes.yaml" + ] + }, + "symfony/security-bundle": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.4", + "ref": "2ae08430db28c8eb4476605894296c82a642028f" + }, + "files": [ + "config/packages/security.yaml", + "config/routes/security.yaml" + ] + }, + "symfony/translation": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.3", + "ref": "e28e27f53663cc34f0be2837aba18e3a1bef8e7b" + }, + "files": [ + "config/packages/translation.yaml", + "translations/.gitignore" + ] + }, + "symfony/twig-bundle": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.4", + "ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877" + }, + "files": [ + "config/packages/twig.yaml", + "templates/base.html.twig" + ] + }, + "symfony/validator": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "c32cfd98f714894c4f128bb99aa2530c1227603c" + }, + "files": [ + "config/packages/validator.yaml" + ] + }, + "symfony/web-profiler-bundle": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.1", + "ref": "e42b3f0177df239add25373083a564e5ead4e13a" + }, + "files": [ + "config/packages/web_profiler.yaml", + "config/routes/web_profiler.yaml" + ] + }, + "symfony/webpack-encore-bundle": { + "version": "1.17", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.10", + "ref": "eff2e505d4557c967b6710fe06bd947ba555cae5" + }, + "files": [ + "assets/app.js", + "assets/bootstrap.js", + "assets/controllers.json", + "assets/controllers/hello_controller.js", + "assets/styles/app.css", + "config/packages/webpack_encore.yaml", + "package.json", + "webpack.config.js" + ] + }, + "symfony/workflow": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.3", + "ref": "3b2f8ca32a07fcb00f899649053943fa3d8bbfb6" + }, + "files": [ + "config/packages/workflow.yaml" + ] + }, + "willdurand/hateoas-bundle": { + "version": "2.6", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "2.0", + "ref": "34df072c6edaa61ae19afb2f3a239f272fecab87" + } + }, + "winzou/state-machine-bundle": { + "version": "v0.6.2" + } +}