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/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 63907e0..77c2b65 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -10,7 +10,7 @@ declare(strict_types=1); $header = <<<'HEADER' -This file is part of Monsieur Biz's for Sylius. +This file is part of Monsieur Biz's Blog plugin for Sylius. (c) Monsieur Biz For the full copyright and license information, please view the LICENSE file that was distributed with this source code. 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..89c78e6 100644 --- a/composer.json +++ b/composer.json @@ -5,8 +5,10 @@ "description": "", "license": "MIT", "require": { - "php": "^8.2", - "sylius/sylius": "^1.12.0 || ^1.13.0" + "php": "^8.1", + "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..1cb30b2 100644 --- a/src/DependencyInjection/MonsieurBizSyliusBlogExtension.php +++ b/src/DependencyInjection/MonsieurBizSyliusBlogExtension.php @@ -1,7 +1,7 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -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..cd17b3e --- /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..3ae3ccf --- /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..4d51dad --- /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..f274148 --- /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..3a6e9e5 --- /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..d592ec5 --- /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..2270d9c --- /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..4c88008 --- /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..7665e28 --- /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..16b1568 --- /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..27e4d30 --- /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..07f5d78 --- /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..f9d0b07 --- /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..46e8a6e --- /dev/null +++ b/src/Fixture/Factory/ArticleFixtureFactory.php @@ -0,0 +1,258 @@ + + * 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) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + 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', LazyOption::randomOnes($this->authorRepository, 2)) + ->setAllowedTypes('authors', ['array']) + ->setNormalizer('authors', function (Options $options, $previousValue): array { + if (null === $previousValue || 0 === \count($previousValue)) { + return []; + } + + $result = []; + foreach ($previousValue as $author) { + if (!\is_object($author)) { + $author = $this->authorRepository->findOneBy(['name' => $author['name']]); + } + if (null !== $author) { + $result[] = $author; + } + } + + return $result; + }) + + ->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) { + 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..debb481 --- /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..b5ea46b --- /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..ae4450f --- /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..24fde17 --- /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..7a82a29 --- /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..ffe8ae6 --- /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..0d86710 --- /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..6b6f743 --- /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..635824a --- /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..a5cc703 --- /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/MonsieurBizSyliusBlogPlugin.php b/src/MonsieurBizSyliusBlogPlugin.php index e086835..02a3f10 100644 --- a/src/MonsieurBizSyliusBlogPlugin.php +++ b/src/MonsieurBizSyliusBlogPlugin.php @@ -1,7 +1,7 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/Repository/ArticleRepository.php b/src/Repository/ArticleRepository.php new file mode 100644 index 0000000..e1e1211 --- /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..bad0d94 --- /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..a3bf999 --- /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..e9c0021 --- /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..0fb752e --- /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..caeb7f7 --- /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" + } +}