diff --git a/phpstan.neon b/phpstan.neon index b76f493..6f1a10d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,5 @@ parameters: - level: max + level: 8 paths: - %rootDir%/src/ diff --git a/src/Form/Type/CaseStudyElementType.php b/src/Form/Type/CaseStudyElementType.php new file mode 100644 index 0000000..5a1b4c9 --- /dev/null +++ b/src/Form/Type/CaseStudyElementType.php @@ -0,0 +1,74 @@ + + * 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\Article; +use MonsieurBiz\SyliusBlogPlugin\Entity\ArticleInterface; +use MonsieurBiz\SyliusBlogPlugin\Repository\ArticleRepositoryInterface; +use Sylius\Bundle\ResourceBundle\Form\DataTransformer\ResourceToIdentifierTransformer; +use Sylius\Component\Channel\Context\ChannelContextInterface; +use Sylius\Component\Locale\Context\LocaleContextInterface; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\IntegerType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\ReversedTransformer; +use Symfony\Component\Validator\Constraints as Assert; + +final class CaseStudyElementType extends AbstractType +{ + /** @phpstan-ignore-next-line */ + public function __construct( + private ArticleRepositoryInterface $articleRepository, + private ChannelContextInterface $channelContext, + private LocaleContextInterface $localeContext, + ) { + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $caseStudies = $this->articleRepository->createShopListQueryBuilderByType( + $this->localeContext->getLocaleCode(), + ArticleInterface::CASE_STUDY_TYPE, + $this->channelContext->getChannel(), + null + ); + + $caseStudies = $caseStudies->orderBy('translation.title')->getQuery()->getResult(); + + $builder + ->add('case_study', EntityType::class, [ + 'class' => Article::class, + 'label' => 'monsieurbiz_blog.ui_element.case_studies_ui_element.fields.case_study', + 'choice_label' => fn (Article $caseStudy) => $caseStudy->getTitle(), + 'choice_value' => fn (?Article $caseStudy) => $caseStudy ? $caseStudy->getId() : null, + 'required' => true, + 'choices' => $caseStudies, + ]) + ->add('position', IntegerType::class, [ + 'label' => 'monsieurbiz_blog.ui_element.case_studies_ui_element.fields.position', + 'required' => true, + 'constraints' => [ + new Assert\NotBlank(), + new Assert\GreaterThan(0), + ], + ]) + ; + + $builder->get('case_study')->addModelTransformer( + new ReversedTransformer(new ResourceToIdentifierTransformer($this->articleRepository, 'id')), + ); + } +} diff --git a/src/Form/Type/UiElement/CaseStudiesUiElementType.php b/src/Form/Type/UiElement/CaseStudiesUiElementType.php new file mode 100644 index 0000000..1d81755 --- /dev/null +++ b/src/Form/Type/UiElement/CaseStudiesUiElementType.php @@ -0,0 +1,66 @@ + + * 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\UiElement; + +use MonsieurBiz\SyliusBlogPlugin\Form\Type\CaseStudyElementType; +use MonsieurBiz\SyliusRichEditorPlugin\Attribute\AsUiElement; +use MonsieurBiz\SyliusRichEditorPlugin\Attribute\TemplatesUiElement; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CollectionType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Validator\Constraints as Assert; + +#[AsUiElement( + code: 'monsieurbiz_blog.case_studies_ui_element', + icon: 'crosshairs', + uiElement: 'MonsieurBiz\SyliusBlogPlugin\UiElement\CaseStudiesUiElement', + title: 'monsieurbiz_blog.ui_element.case_studies_ui_element.title', + description: 'monsieurbiz_blog.ui_element.case_studies_ui_element.description', + templates: new TemplatesUiElement( + adminRender: '@MonsieurBizSyliusBlogPlugin/Admin/UiElement/case_studies.html.twig', + frontRender: '@MonsieurBizSyliusBlogPlugin/Shop/UiElement/case_studies.html.twig', + ), + tags: [], + wireframe: 'case-studies', +)] +class CaseStudiesUiElementType extends AbstractType +{ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('title', TextType::class, [ + 'label' => 'monsieurbiz_blog.ui_element.case_studies_ui_element.fields.title', + 'required' => false, + ]) + ->add('case_studies', CollectionType::class, [ + 'label' => 'monsieurbiz_blog.ui_element.case_studies_ui_element.fields.case_studies', + 'entry_type' => CaseStudyElementType::class, + 'prototype_name' => '__case_study__', + 'allow_add' => true, + 'allow_delete' => true, + 'by_reference' => false, + 'delete_empty' => true, + 'attr' => [ + 'class' => 'ui segment secondary collection--flex', + ], + 'constraints' => [ + new Assert\Count(['min' => 1]), + new Assert\Valid(), + ], + ]) + ; + } +} diff --git a/src/Repository/ArticleRepository.php b/src/Repository/ArticleRepository.php index c3f7d2f..0c1bacb 100644 --- a/src/Repository/ArticleRepository.php +++ b/src/Repository/ArticleRepository.php @@ -58,7 +58,6 @@ public function createShopListQueryBuilderByType(string $localeCode, string $typ public function findAllEnabledAndPublishedByTag(string $localeCode, string $type, ChannelInterface $channel, TagInterface $tag, int $limit): array { - /** @phpstan-ignore-next-line */ return $this->createShopListQueryBuilderByType($localeCode, $type, $channel, $tag) ->setMaxResults($limit) ->getQuery() @@ -68,7 +67,6 @@ public function findAllEnabledAndPublishedByTag(string $localeCode, string $type public function findOnePublishedBySlug(string $slug, string $localeCode, string $type, ChannelInterface $channel): ?ArticleInterface { - /** @phpstan-ignore-next-line */ return $this->createListQueryBuilderByType($localeCode, $type) ->andWhere('translation.slug = :slug') ->andWhere(':channel MEMBER OF ba.channels') @@ -84,7 +82,6 @@ public function findOnePublishedBySlug(string $slug, string $localeCode, string public function findAllEnabledAndPublishedByAuthor(string $localeCode, string $type, ChannelInterface $channel, AuthorInterface $author, int $limit): array { - /** @phpstan-ignore-next-line */ return $this->createListQueryBuilderByType($localeCode, $type) ->andWhere(':channel MEMBER OF ba.channels') ->andWhere('ba.enabled = true') @@ -99,4 +96,21 @@ public function findAllEnabledAndPublishedByAuthor(string $localeCode, string $t ->getResult() ; } + + public function findEnabledAndPublishedByIds(array $articleIds, string $localeCode, string $type, ChannelInterface $channel, ?int $number = null): array + { + $queryBuilder = $this->createShopListQueryBuilderByType($localeCode, $type, $channel, null) + ->andWhere('ba.id in (:articleIds)') + ->addOrderBy('ba.publishedAt', 'desc') + ->setParameter('articleIds', $articleIds) + ; + + if (null !== $number) { + $queryBuilder->setMaxResults($number); + } + + return $queryBuilder->getQuery() + ->getResult() + ; + } } diff --git a/src/Repository/ArticleRepositoryInterface.php b/src/Repository/ArticleRepositoryInterface.php index 18026f9..0ea881e 100644 --- a/src/Repository/ArticleRepositoryInterface.php +++ b/src/Repository/ArticleRepositoryInterface.php @@ -37,4 +37,6 @@ public function findAllEnabledAndPublishedByTag(string $localeCode, string $type public function findOnePublishedBySlug(string $slug, string $localeCode, string $type, ChannelInterface $channel): ?ArticleInterface; public function findAllEnabledAndPublishedByAuthor(string $localeCode, string $type, ChannelInterface $channel, AuthorInterface $author, int $limit): array; + + public function findEnabledAndPublishedByIds(array $articleIds, string $localeCode, string $type, ChannelInterface $channel, ?int $number = null): array; } diff --git a/src/Repository/TagRepository.php b/src/Repository/TagRepository.php index 0fb752e..67b2ac4 100644 --- a/src/Repository/TagRepository.php +++ b/src/Repository/TagRepository.php @@ -26,7 +26,6 @@ final class TagRepository extends EntityRepository implements TagRepositoryInter { public function findRootNodes(): array { - /** @phpstan-ignore-next-line */ return $this->createQueryBuilder('o') ->addOrderBy('o.position') ->getQuery() @@ -71,7 +70,6 @@ public function createEnabledListQueryBuilder(string $localeCode): QueryBuilder */ public function findOneByName(string $name, string $localeCode): ?TagInterface { - /** @phpstan-ignore-next-line */ return $this->createListQueryBuilder($localeCode) ->andWhere('translation.name = :name') ->setParameter('name', $name) @@ -85,7 +83,6 @@ public function findOneByName(string $name, string $localeCode): ?TagInterface */ public function findOneBySlug(string $slug, string $localeCode): ?TagInterface { - /** @phpstan-ignore-next-line */ return $this->createListQueryBuilder($localeCode) ->andWhere('translation.slug = :slug') ->setParameter('slug', $slug) diff --git a/src/Resources/translations/messages.en.yaml b/src/Resources/translations/messages.en.yaml index f89f721..7da7d87 100644 --- a/src/Resources/translations/messages.en.yaml +++ b/src/Resources/translations/messages.en.yaml @@ -46,3 +46,13 @@ monsieurbiz_blog: read_more: Read more new_case_study: New case study edit_case_study: Edit case study + no_results_to_display: No results to display + ui_element: + case_studies_ui_element: + title: Case studies Element + description: Display a list of case studies. + fields: + title: Title + case_studies: Case studies + case_study: Case study + position: Position diff --git a/src/Resources/translations/messages.fr.yaml b/src/Resources/translations/messages.fr.yaml index 4a76821..8240182 100644 --- a/src/Resources/translations/messages.fr.yaml +++ b/src/Resources/translations/messages.fr.yaml @@ -46,3 +46,13 @@ monsieurbiz_blog: read_more: Lire la suite new_case_study: Nouvelle étude de cas edit_case_study: Modifier l'étude de cas + no_results_to_display: Aucun résultat à afficher + ui_element: + case_studies_ui_element: + title: Lame Études de cas + description: Affiche une liste d'études de cas. + fields: + title: Titre + case_studies: Études de cas + case_study: Étude de cas + position: Position diff --git a/src/Resources/views/Admin/Article/_image.html.twig b/src/Resources/views/Admin/Article/_image.html.twig new file mode 100644 index 0000000..6443eea --- /dev/null +++ b/src/Resources/views/Admin/Article/_image.html.twig @@ -0,0 +1,18 @@ +{% set filter = filter|default('monsieurbiz_blog_image_thumbnail') %} +{% set placeholder = placeholder|default('200x200.png') %} + +{# Thumbnail display #} +{% if article.thumbnailImage and filter == 'monsieurbiz_blog_image_thumbnail' %} + {% set path = article.thumbnailImage|imagine_filter(filter) %} +{# Image display or thumbnail fallback on image #} +{% elseif 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/Admin/UiElement/case_studies.html.twig b/src/Resources/views/Admin/UiElement/case_studies.html.twig new file mode 100644 index 0000000..85788cb --- /dev/null +++ b/src/Resources/views/Admin/UiElement/case_studies.html.twig @@ -0,0 +1,41 @@ +{# + UI Element template + type: case_studies + element fields : + title + case_studies + element methods: + getCaseStudies +#} +{% import '@SyliusUi/Macro/messages.html.twig' as messages %} + +{% set case_studies = ui_element.getCaseStudies(element) %} + +{% if case_studies|length > 0 %} + + {% if element.title|default('') is not empty %} +
+ {{ element.title }} +
+ {% endif %} + +
+ {% for case_study in case_studies %} +
+
+ {% include '@MonsieurBizSyliusBlogPlugin/Admin/Article/_image.html.twig' with { 'article' : case_study} %} +
+ +
+ {% endfor %} +
+{% else %} + {{ messages.info('monsieurbiz_blog.ui.no_results_to_display') }} +{% endif %} diff --git a/src/Resources/views/Shop/UiElement/case_studies.html.twig b/src/Resources/views/Shop/UiElement/case_studies.html.twig new file mode 100644 index 0000000..d193201 --- /dev/null +++ b/src/Resources/views/Shop/UiElement/case_studies.html.twig @@ -0,0 +1,39 @@ +{# + UI Element template + type: case_studies + element fields : + title + case_studies + element methods: + getCaseStudies +#} +{% import '@SyliusUi/Macro/messages.html.twig' as messages %} + +{% set case_studies = ui_element.getCaseStudies(element) %} + +{% if case_studies|length > 0 %} + + {% if element.title|default('') is not empty %} +
+ {{ element.title }} +
+ {% endif %} + +
+ {% for case_study in case_studies %} +
+
+ {% include '@MonsieurBizSyliusBlogPlugin/Admin/Article/_image.html.twig' with { 'article' : case_study} %} +
+ +
+ {% endfor %} +
+{% endif %} diff --git a/src/Resources/views/Wireframe/case-studies.svg.twig b/src/Resources/views/Wireframe/case-studies.svg.twig new file mode 100644 index 0000000..3abf037 --- /dev/null +++ b/src/Resources/views/Wireframe/case-studies.svg.twig @@ -0,0 +1,3 @@ + + + diff --git a/src/UiElement/CaseStudiesUiElement.php b/src/UiElement/CaseStudiesUiElement.php new file mode 100644 index 0000000..43a5b78 --- /dev/null +++ b/src/UiElement/CaseStudiesUiElement.php @@ -0,0 +1,73 @@ + + * 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\UiElement; + +use MonsieurBiz\SyliusBlogPlugin\Entity\ArticleInterface; +use MonsieurBiz\SyliusBlogPlugin\Repository\ArticleRepositoryInterface; +use MonsieurBiz\SyliusRichEditorPlugin\UiElement\UiElementInterface; +use MonsieurBiz\SyliusRichEditorPlugin\UiElement\UiElementTrait; +use Sylius\Component\Channel\Context\ChannelContextInterface; +use Sylius\Component\Locale\Context\LocaleContextInterface; + +final class CaseStudiesUiElement implements UiElementInterface +{ + use UiElementTrait; + + /** @phpstan-ignore-next-line */ + public function __construct( + private ArticleRepositoryInterface $articleRepository, + private LocaleContextInterface $localeContext, + private ChannelContextInterface $channelContext, + ) { + } + + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * + * @return ArticleInterface[] + */ + public function getCaseStudies(array $element): array + { + // A case study in array contains `case_study` (the ID) and `position` keys + $caseStudiesArray = $element['case_studies'] ?? []; + + // List the IDs to retrieve from repository + $caseStudyIds = array_map(function ($caseStudy) { + return $caseStudy['case_study']; + }, $caseStudiesArray); + + // Prepare sorting + usort($caseStudiesArray, function ($caseStudyA, $caseStudyB) { + return $caseStudyA['position'] <=> $caseStudyB['position']; + }); + + $result = []; + // Retrieve case studies objects + if (\count($caseStudiesArray) > 0 && \count($caseStudyIds) > 0) { + $caseStudies = $this->articleRepository->findEnabledAndPublishedByIds( + $caseStudyIds, + $this->localeContext->getLocaleCode(), + ArticleInterface::CASE_STUDY_TYPE, + $this->channelContext->getChannel() + ); + foreach ($caseStudiesArray as $caseStudyArray) { + foreach ($caseStudies as $caseStudy) { + if ($caseStudy->getId() === $caseStudyArray['case_study']) { + $result[] = $caseStudy; + } + } + } + } + + return $result; + } +}