feat: Themes (#257)
* feat: add trait to unify get by id or slug logic * feat: add Theme resource * fix: categories and themes * feat: update api docs for projects to themes relationship * feat: update doctrine relationship of projects to themes * feat: add v3 categories to v4 themes pumping during projects pumping
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20261007154315 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add theme tables';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE project_theme (project_id INT NOT NULL, theme_id INT NOT NULL, INDEX IDX_8420EE96166D1F9C (project_id), INDEX IDX_8420EE9659027487 (theme_id), PRIMARY KEY(project_id, theme_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||
$this->addSql('CREATE TABLE theme (id INT AUTO_INCREMENT NOT NULL, slug VARCHAR(56) NOT NULL, name VARCHAR(255) NOT NULL, locales JSON NOT NULL COMMENT \'(DC2Type:json)\', UNIQUE INDEX UNIQ_9775E708989D9B62 (slug), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||
$this->addSql('ALTER TABLE project_theme ADD CONSTRAINT FK_8420EE96166D1F9C FOREIGN KEY (project_id) REFERENCES project (id) ON DELETE CASCADE');
|
||||
$this->addSql('ALTER TABLE project_theme ADD CONSTRAINT FK_8420EE9659027487 FOREIGN KEY (theme_id) REFERENCES theme (id) ON DELETE CASCADE');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE project_theme DROP FOREIGN KEY FK_8420EE96166D1F9C');
|
||||
$this->addSql('ALTER TABLE project_theme DROP FOREIGN KEY FK_8420EE9659027487');
|
||||
$this->addSql('DROP TABLE project_theme');
|
||||
$this->addSql('DROP TABLE theme');
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,8 @@ use App\State\CategoryStateProvider;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* A Category can be used by other resources as a "topic intent".\
|
||||
* For example. Projects might relate with up to 2 Categories, which are used by the Project
|
||||
* as a way to describe itself and can be used to discover similar Projects.\
|
||||
* A Category, like in books, is a way of grouping resources by type, genre, audience, or subject, for example:
|
||||
* science-fiction, biography, fantasy, etc.\
|
||||
* \
|
||||
* Categories can only be modified by users with the role "ROLE_ADMIN", but can usually
|
||||
* be referenced by non-admin users in their own resources, such as Project owners.
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\ApiResource\Accounting\AccountingApiResource;
|
||||
use App\ApiResource\CategoryApiResource;
|
||||
use App\ApiResource\LocalizedApiResourceTrait;
|
||||
use App\ApiResource\Matchfunding\MatchCallSubmissionApiResource;
|
||||
use App\ApiResource\ThemeApiResource;
|
||||
use App\ApiResource\TimestampedCreationApiResource;
|
||||
use App\ApiResource\TimestampedUpdationApiResource;
|
||||
use App\ApiResource\User\UserApiResource;
|
||||
@@ -134,6 +135,16 @@ class ProjectApiResource
|
||||
#[API\ApiFilter(filterClass: SearchFilter::class, strategy: 'exact')]
|
||||
public array $categories;
|
||||
|
||||
/**
|
||||
* A list of the available Themes of this Project.
|
||||
*
|
||||
* @var ThemeApiResource[]
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[API\ApiProperty(writableLink: false)]
|
||||
#[API\ApiFilter(filterClass: SearchFilter::class, strategy: 'exact')]
|
||||
public array $themes;
|
||||
|
||||
/**
|
||||
* ISO 3166 data about the Project's territory of interest.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\ApiResource;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Doctrine\Orm\State\Options;
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\Entity\Theme;
|
||||
use App\State\ApiResourceStateProcessor;
|
||||
use App\State\ApiResourceStateProvider;
|
||||
use App\State\ThemeStateProvider;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* A Theme, like in books, works as a "topic intent" grouping, for example:
|
||||
* friendship, good vs. evil, power and corruption, etc.\
|
||||
* \
|
||||
* Themes can only be modified by users with the role "ROLE_ADMIN", but can usually
|
||||
* be referenced by non-admin users in their own resources, such as Project owners.
|
||||
*/
|
||||
#[API\ApiResource(
|
||||
shortName: 'Theme',
|
||||
stateOptions: new Options(entityClass: Theme::class),
|
||||
provider: ApiResourceStateProvider::class,
|
||||
processor: ApiResourceStateProcessor::class,
|
||||
)]
|
||||
#[API\GetCollection()]
|
||||
#[API\Post(security: 'is_granted("ROLE_ADMIN")')]
|
||||
#[API\Get(
|
||||
provider: ThemeStateProvider::class,
|
||||
uriTemplate: '/themes/{idOrSlug}',
|
||||
uriVariables: [
|
||||
'idOrSlug' => new API\Link(
|
||||
description: 'Theme identifier or slug',
|
||||
),
|
||||
]
|
||||
)]
|
||||
#[API\Patch(security: 'is_granted("ROLE_ADMIN")')]
|
||||
#[API\Delete(security: 'is_granted("ROLE_ADMIN")')]
|
||||
class ThemeApiResource
|
||||
{
|
||||
use LocalizedApiResourceTrait;
|
||||
|
||||
/**
|
||||
* This value will identify this Theme in relationships with other resources.
|
||||
*/
|
||||
#[API\ApiProperty(identifier: true)]
|
||||
public string $id;
|
||||
|
||||
/**
|
||||
* A unique, non white space, string identifier for this Theme.
|
||||
*/
|
||||
#[API\ApiProperty(writable: false)]
|
||||
#[API\ApiFilter(SearchFilter::class, strategy: 'exact')]
|
||||
public string $slug;
|
||||
|
||||
/**
|
||||
* A human-readable self-descriptive string of what this Theme is about.
|
||||
*/
|
||||
#[Assert\NotBlank()]
|
||||
#[API\ApiProperty()]
|
||||
#[API\ApiFilter(SearchFilter::class, strategy: 'partial')]
|
||||
public string $name;
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace App\Benzina;
|
||||
|
||||
use App\Entity\Theme;
|
||||
use App\Repository\ThemeRepository;
|
||||
use Symfony\Contracts\Service\Attribute\Required;
|
||||
|
||||
trait CategorizationPumpTrait
|
||||
{
|
||||
private ThemeRepository $themeRepository;
|
||||
|
||||
private const THEMES = [
|
||||
'education' => 'Educación',
|
||||
'culture' => 'Cultura',
|
||||
'journalism' => 'Periodismo',
|
||||
'science-and-tech' => 'Ciencia y Tecnología',
|
||||
'rural-and-ecology' => 'Desarrollo rural y Transición ecológica',
|
||||
'health-and-cares' => 'Salud y Cuidados',
|
||||
'sustainability' => 'Sostenibilidad',
|
||||
'democracy' => 'Derechos y Democracia',
|
||||
'social-economy' => 'Economía social',
|
||||
];
|
||||
|
||||
#[Required]
|
||||
public function setThemeRepository(ThemeRepository $themeRepository)
|
||||
{
|
||||
$this->themeRepository = $themeRepository;
|
||||
}
|
||||
|
||||
public function getTheme(string $slug): Theme
|
||||
{
|
||||
$theme = $this->themeRepository->findOneBy(['slug' => $slug]);
|
||||
|
||||
if (!$theme) {
|
||||
$theme = new Theme();
|
||||
$theme->setSlug($slug);
|
||||
$theme->setName(self::THEMES[$slug]);
|
||||
}
|
||||
|
||||
$theme->addLocale('es');
|
||||
|
||||
return $theme;
|
||||
}
|
||||
|
||||
private function matchCategories(array $categories, array $names): bool
|
||||
{
|
||||
$categories = array_map(fn($c) => $c['name'], $categories);
|
||||
$matches = 0;
|
||||
|
||||
foreach ($names as $name) {
|
||||
if (in_array($name, $categories)) {
|
||||
$matches = $matches + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $matches === count($names);
|
||||
}
|
||||
|
||||
private function mapOldCategoriesToThemes(string $socialCommitmentName, array $categories): array
|
||||
{
|
||||
$themes = [];
|
||||
|
||||
if (
|
||||
$socialCommitmentName === 'Solidario'
|
||||
&& $this->matchCategories($categories, ['Social'])
|
||||
) {
|
||||
$themes[] = $this->getTheme('democracy');
|
||||
}
|
||||
|
||||
if (
|
||||
$socialCommitmentName === 'Software libre'
|
||||
&& $this->matchCategories($categories, ['Tecnológico', 'Científico'])
|
||||
) {
|
||||
$themes[] = $this->getTheme('science-and-tech');
|
||||
}
|
||||
|
||||
if (
|
||||
$socialCommitmentName === 'Generar empleo'
|
||||
&& $this->matchCategories($categories, ['Emprendedor', 'Social'])
|
||||
) {
|
||||
$themes[] = $this->getTheme('social-economy');
|
||||
}
|
||||
|
||||
if (
|
||||
$socialCommitmentName === 'Periodismo independiente'
|
||||
&& $this->matchCategories($categories, ['Comunicativo'])
|
||||
) {
|
||||
$themes[] = $this->getTheme('journalism');
|
||||
}
|
||||
|
||||
if (
|
||||
$socialCommitmentName === 'Educativo'
|
||||
&& $this->matchCategories($categories, ['Educativo'])
|
||||
) {
|
||||
$themes[] = $this->getTheme('education');
|
||||
}
|
||||
|
||||
if (
|
||||
$socialCommitmentName === 'Crear cultura'
|
||||
&& $this->matchCategories($categories, ['Cultural'])
|
||||
) {
|
||||
$themes[] = $this->getTheme('culture');
|
||||
}
|
||||
|
||||
if (
|
||||
$socialCommitmentName === 'Acción por el clima'
|
||||
&& $this->matchCategories($categories, ['Ecológico'])
|
||||
) {
|
||||
$themes[] = $this->getTheme('sustainability');
|
||||
}
|
||||
|
||||
if (
|
||||
$socialCommitmentName === 'Datos abiertos'
|
||||
&& $this->matchCategories($categories, ['Científico', 'Comunicativo'])
|
||||
) {
|
||||
$themes[] = $this->getTheme('journalism');
|
||||
}
|
||||
|
||||
if (
|
||||
$socialCommitmentName === 'Reforzar valores democráticos'
|
||||
&& $this->matchCategories($categories, ['Social'])
|
||||
) {
|
||||
$themes[] = $this->getTheme('democracy');
|
||||
}
|
||||
|
||||
if (
|
||||
$socialCommitmentName === 'Igualdad de Género'
|
||||
&& $this->matchCategories($categories, ['Social'])
|
||||
) {
|
||||
$themes[] = $this->getTheme('democracy');
|
||||
}
|
||||
|
||||
if (
|
||||
$socialCommitmentName === 'Salud y Cuidados'
|
||||
&& $this->matchCategories($categories, ['Social'])
|
||||
) {
|
||||
$themes[] = $this->getTheme('health-and-cares');
|
||||
}
|
||||
|
||||
if (
|
||||
$socialCommitmentName === 'Energía y sostenibilidad'
|
||||
&& $this->matchCategories($categories, ['Científico', 'Ecológico'])
|
||||
) {
|
||||
$themes[] = $this->getTheme('science-and-tech');
|
||||
}
|
||||
|
||||
if (
|
||||
$socialCommitmentName === 'Desarrollo agrorural'
|
||||
&& $this->matchCategories($categories, ['Ecológico', 'Emprendedor'])
|
||||
) {
|
||||
$themes[] = $this->getTheme('rural-and-ecology');
|
||||
}
|
||||
|
||||
return $themes;
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ class ProjectsPump implements PumpInterface
|
||||
use ProjectsPumpTrait;
|
||||
use LocalizedPumpTrait;
|
||||
use TerritoryPumpTrait;
|
||||
use CategorizationPumpTrait;
|
||||
|
||||
public function __construct(
|
||||
private ProjectRepository $projectRepository,
|
||||
@@ -106,6 +107,11 @@ class ProjectsPump implements PumpInterface
|
||||
$project->setDescGoal($record['motivation']);
|
||||
$project->setDescTeam($record['related']);
|
||||
|
||||
$themes = $this->getProjectThemes($record, $context);
|
||||
foreach ($themes as $theme) {
|
||||
$project->addTheme($theme);
|
||||
}
|
||||
|
||||
$this->setPreventFlushAndClear(true);
|
||||
$this->persist($project, $context);
|
||||
|
||||
@@ -407,4 +413,23 @@ class ProjectsPump implements PumpInterface
|
||||
|
||||
return $collaborations;
|
||||
}
|
||||
|
||||
private function getProjectThemes(array $record, array $context): array
|
||||
{
|
||||
$scQuery = $this->getDbConnection($context)->prepare(
|
||||
'SELECT * FROM `social_commitment` sc WHERE sc.id = :scId'
|
||||
);
|
||||
|
||||
$scQuery->execute(['scId' => $record['social_commitment']]);
|
||||
$socialCommitment = $scQuery->fetch();
|
||||
|
||||
$catsQuery = $this->getDbConnection($context)->prepare(
|
||||
'SELECT * FROM `category` c INNER JOIN `project_category` pc ON c.id = pc.category WHERE pc.project = :project'
|
||||
);
|
||||
|
||||
$catsQuery->execute(['project' => $record['id']]);
|
||||
$categories = $catsQuery->fetchAll();
|
||||
|
||||
return $this->mapOldCategoriesToThemes($socialCommitment['name'] ?? '', $categories);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
trait CategorizedInputDtoTrait
|
||||
{
|
||||
public const array CATEGORIES_OPENAPI_CONTEXT = [
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'string',
|
||||
'format' => 'iri-reference',
|
||||
'example' => '/categories/cooperativism',
|
||||
],
|
||||
'example' => [
|
||||
'/categories/open-source-software',
|
||||
'/categories/cooperativism',
|
||||
],
|
||||
];
|
||||
|
||||
public const array THEMES_OPENAPI_CONTEXT = [
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'string',
|
||||
'format' => 'iri-reference',
|
||||
'example' => '/themes/music',
|
||||
],
|
||||
'example' => [
|
||||
'/themes/music',
|
||||
'/themes/art',
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
trait CategoryInputDtoTrait
|
||||
{
|
||||
public const array CATEGORIES_OPENAPI_CONTEXT = [
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'string',
|
||||
'format' => 'iri-reference',
|
||||
'example' => '/categories/music',
|
||||
],
|
||||
'example' => [
|
||||
'/categories/music',
|
||||
'/categories/art',
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Dto;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\CategoryApiResource;
|
||||
use App\ApiResource\ThemeApiResource;
|
||||
use App\Entity\Project\Project;
|
||||
use App\Entity\Project\ProjectCalendar;
|
||||
use App\Entity\Project\ProjectStatus;
|
||||
@@ -13,7 +14,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
class ProjectCreationDto
|
||||
{
|
||||
use CategoryInputDtoTrait;
|
||||
use CategorizedInputDtoTrait;
|
||||
|
||||
/**
|
||||
* Main headline for the Project. Must include at least one character between a-Z.
|
||||
@@ -40,6 +41,15 @@ class ProjectCreationDto
|
||||
#[API\ApiProperty(writableLink: false, openapiContext: self::CATEGORIES_OPENAPI_CONTEXT)]
|
||||
public array $categories;
|
||||
|
||||
/**
|
||||
* List of Themes.
|
||||
*
|
||||
* @var ThemeApiResource[]
|
||||
*/
|
||||
#[Assert\Count(max: 1)]
|
||||
#[API\ApiProperty(writableLink: false, openapiContext: self::THEMES_OPENAPI_CONTEXT)]
|
||||
public array $themes;
|
||||
|
||||
/**
|
||||
* ISO 3166 data about the Project's territory of interest.
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Dto;
|
||||
|
||||
use ApiPlatform\Metadata as API;
|
||||
use App\ApiResource\CategoryApiResource;
|
||||
use App\ApiResource\ThemeApiResource;
|
||||
use App\Entity\Project\Project;
|
||||
use App\Entity\Project\ProjectCalendar;
|
||||
use App\Entity\Project\ProjectDeadline;
|
||||
@@ -15,7 +16,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
class ProjectUpdationDto
|
||||
{
|
||||
use CategoryInputDtoTrait;
|
||||
use CategorizedInputDtoTrait;
|
||||
|
||||
#[API\ApiProperty(identifier: true, writable: false)]
|
||||
public int $id;
|
||||
@@ -47,6 +48,15 @@ class ProjectUpdationDto
|
||||
#[API\ApiProperty(writableLink: false, openapiContext: self::CATEGORIES_OPENAPI_CONTEXT)]
|
||||
public array $categories;
|
||||
|
||||
/**
|
||||
* List of Themes.
|
||||
*
|
||||
* @var ThemeApiResource[]
|
||||
*/
|
||||
#[Assert\Count(max: 1)]
|
||||
#[API\ApiProperty(writableLink: false, openapiContext: self::THEMES_OPENAPI_CONTEXT)]
|
||||
public array $themes;
|
||||
|
||||
/**
|
||||
* ISO 3166 data about the Project's territory of interest.
|
||||
*/
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Entity\Matchfunding\MatchCallSubmission;
|
||||
use App\Entity\Matchfunding\MatchCallSubmissionStatus;
|
||||
use App\Entity\MigratedTrait;
|
||||
use App\Entity\Territory;
|
||||
use App\Entity\Theme;
|
||||
use App\Entity\User\User;
|
||||
use App\Entity\UserOwnedInterface;
|
||||
use App\Entity\UserOwnedTrait;
|
||||
@@ -175,6 +176,12 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?array $links = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Theme>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Theme::class, cascade: ['persist'])]
|
||||
private Collection $themes;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->accounting = Accounting::of($this);
|
||||
@@ -186,6 +193,7 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized
|
||||
$this->categories = new ArrayCollection();
|
||||
$this->collaborations = new ArrayCollection();
|
||||
$this->reviews = new ArrayCollection();
|
||||
$this->themes = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -651,4 +659,28 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Theme>
|
||||
*/
|
||||
public function getThemes(): Collection
|
||||
{
|
||||
return $this->themes;
|
||||
}
|
||||
|
||||
public function addTheme(Theme $theme): static
|
||||
{
|
||||
if (!$this->themes->contains($theme)) {
|
||||
$this->themes->add($theme);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeTheme(Theme $theme): static
|
||||
{
|
||||
$this->themes->removeElement($theme);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\ThemeRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Gedmo\Mapping\Annotation as Gedmo;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ThemeRepository::class)]
|
||||
class Theme
|
||||
{
|
||||
use LocalizedTrait;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 56, unique: true)]
|
||||
#[Gedmo\Slug(fields: ['name'])]
|
||||
private ?string $slug = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Gedmo\Translatable()]
|
||||
private ?string $name = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getSlug(): ?string
|
||||
{
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
public function setSlug(string $slug): static
|
||||
{
|
||||
$this->slug = $slug;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,13 @@ trait RedocExtensionsTrait
|
||||
'ProjectSupport',
|
||||
'ProjectCollaboration',
|
||||
'ProjectCollaborationCandidacy',
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'Categorization',
|
||||
'tags' => [
|
||||
'Category',
|
||||
'Theme',
|
||||
],
|
||||
],
|
||||
[
|
||||
|
||||
@@ -11,6 +11,8 @@ use Doctrine\Persistence\ManagerRegistry;
|
||||
*/
|
||||
class CategoryRepository extends ServiceEntityRepository
|
||||
{
|
||||
use FindByIdOrSlugTrait;
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Category::class);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
/**
|
||||
* @template T of object
|
||||
*
|
||||
* @mixin ServiceEntityRepository
|
||||
*/
|
||||
trait FindByIdOrSlugTrait
|
||||
{
|
||||
/**
|
||||
* @return T|null
|
||||
*/
|
||||
public function findOneByIdOrSlug(mixed $idOrSlug): ?object
|
||||
{
|
||||
if (\ctype_digit((string) $idOrSlug)) {
|
||||
return $this->find($idOrSlug);
|
||||
}
|
||||
|
||||
return $this->findOneBy(['slug' => $idOrSlug]);
|
||||
}
|
||||
|
||||
public function getByIdOrSlugQuery(mixed $idOrSlug): QueryBuilder
|
||||
{
|
||||
/** @var QueryBuilder */
|
||||
$queryBuilder = $this->createQueryBuilder('o');
|
||||
$queryBuilder->where(\ctype_digit((string) $idOrSlug) ? 'o.id = :idOrSlugField' : 'o.slug = :idOrSlugField');
|
||||
$queryBuilder->setParameter('idOrSlugField', $idOrSlug);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Repository\Project;
|
||||
|
||||
use App\Entity\Accounting\Accounting;
|
||||
use App\Entity\Project\Project;
|
||||
use App\Repository\FindByIdOrSlugTrait;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
@@ -17,6 +18,8 @@ use Doctrine\Persistence\ManagerRegistry;
|
||||
*/
|
||||
class ProjectRepository extends ServiceEntityRepository
|
||||
{
|
||||
use FindByIdOrSlugTrait;
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Project::class);
|
||||
@@ -31,15 +34,6 @@ class ProjectRepository extends ServiceEntityRepository
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
|
||||
public function findOneByIdOrSlug(mixed $idOrSlug): ?Project
|
||||
{
|
||||
if (\is_numeric($idOrSlug)) {
|
||||
return $this->find($idOrSlug);
|
||||
}
|
||||
|
||||
return $this->findOneBy(['slug' => $idOrSlug]);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return Project[] Returns an array of Project objects
|
||||
// */
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Theme;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Theme>
|
||||
*/
|
||||
class ThemeRepository extends ServiceEntityRepository
|
||||
{
|
||||
use FindByIdOrSlugTrait;
|
||||
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Theme::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return Theme[] Returns an array of Theme objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('t')
|
||||
// ->andWhere('t.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('t.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?Theme
|
||||
// {
|
||||
// return $this->createQueryBuilder('t')
|
||||
// ->andWhere('t.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
@@ -20,20 +20,14 @@ class CategoryStateProvider implements ProviderInterface
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
|
||||
{
|
||||
$idOrSlug = $uriVariables['idOrSlug'];
|
||||
|
||||
$queryBuilder = $this->categoryRepository->createQueryBuilder('c');
|
||||
$queryBuilder->where(\is_numeric($idOrSlug) ? 'c.id = :value' : 'c.slug = :value');
|
||||
$queryBuilder->setParameter('value', $idOrSlug);
|
||||
|
||||
$queryBuilder = $this->categoryRepository->getByIdOrSlugQuery($uriVariables['idOrSlug']);
|
||||
$query = $this->addLocalizationHints($queryBuilder, $this->getAcceptedLanguages($context));
|
||||
$category = $query->getOneOrNullResult();
|
||||
|
||||
$project = $query->getOneOrNullResult();
|
||||
|
||||
if ($project === null) {
|
||||
if ($category === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->autoMapper->map($project, CategoryApiResource::class);
|
||||
return $this->autoMapper->map($category, CategoryApiResource::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,14 +20,8 @@ class ProjectStateProvider implements ProviderInterface
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
|
||||
{
|
||||
$idOrSlug = $uriVariables['idOrSlug'];
|
||||
|
||||
$queryBuilder = $this->projectRepository->createQueryBuilder('p');
|
||||
$queryBuilder->where(\is_numeric($idOrSlug) ? 'p.id = :value' : 'p.slug = :value');
|
||||
$queryBuilder->setParameter('value', $idOrSlug);
|
||||
|
||||
$queryBuilder = $this->projectRepository->getByIdOrSlugQuery($uriVariables['idOrSlug']);
|
||||
$query = $this->addLocalizationHints($queryBuilder, $this->getAcceptedLanguages($context));
|
||||
|
||||
$project = $query->getOneOrNullResult();
|
||||
|
||||
if ($project === null) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\ApiResource\ThemeApiResource;
|
||||
use App\Doctrine\LocalizedExtensionTrait;
|
||||
use App\Mapping\AutoMapper;
|
||||
use App\Repository\ThemeRepository;
|
||||
|
||||
class ThemeStateProvider implements ProviderInterface
|
||||
{
|
||||
use LocalizedExtensionTrait;
|
||||
|
||||
public function __construct(
|
||||
private ThemeRepository $themeRepository,
|
||||
private AutoMapper $autoMapper,
|
||||
) {}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
|
||||
{
|
||||
$queryBuilder = $this->themeRepository->getByIdOrSlugQuery($uriVariables['idOrSlug']);
|
||||
$query = $this->addLocalizationHints($queryBuilder, $this->getAcceptedLanguages($context));
|
||||
$theme = $query->getOneOrNullResult();
|
||||
|
||||
if ($theme === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->autoMapper->map($theme, ThemeApiResource::class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user