diff --git a/config/schema_draft.php b/config/schema_draft.php index e413877..144bdf5 100644 --- a/config/schema_draft.php +++ b/config/schema_draft.php @@ -4,6 +4,7 @@ use Fau\DegreeProgram\Common\Domain\AdmissionRequirement; use Fau\DegreeProgram\Common\Domain\AdmissionRequirements; +use Fau\DegreeProgram\Common\Domain\CampoKeys; use Fau\DegreeProgram\Common\Domain\Content; use Fau\DegreeProgram\Common\Domain\ContentItem; use Fau\DegreeProgram\Common\Domain\Degree; @@ -150,5 +151,6 @@ DegreeProgram::STUDENT_INITIATIVES => MultilingualLink::SCHEMA, DegreeProgram::APPLY_NOW_LINK => MultilingualLink::SCHEMA, DegreeProgram::ENTRY_TEXT => MultilingualString::SCHEMA, + DegreeProgram::CAMPO_KEYS => CampoKeys::SCHEMA, ], ]; diff --git a/config/schema_publish.php b/config/schema_publish.php index 942ea54..dba036f 100644 --- a/config/schema_publish.php +++ b/config/schema_publish.php @@ -4,6 +4,7 @@ use Fau\DegreeProgram\Common\Domain\AdmissionRequirement; use Fau\DegreeProgram\Common\Domain\AdmissionRequirements; +use Fau\DegreeProgram\Common\Domain\CampoKeys; use Fau\DegreeProgram\Common\Domain\Content; use Fau\DegreeProgram\Common\Domain\ContentItem; use Fau\DegreeProgram\Common\Domain\Degree; @@ -155,5 +156,6 @@ DegreeProgram::STUDENT_INITIATIVES => MultilingualLink::SCHEMA, DegreeProgram::APPLY_NOW_LINK => MultilingualLink::SCHEMA_REQUIRED, DegreeProgram::ENTRY_TEXT => MultilingualString::SCHEMA_REQUIRED, + DegreeProgram::CAMPO_KEYS => CampoKeys::SCHEMA_REQUIRED, ], ]; diff --git a/src/Application/DegreeProgramViewRaw.php b/src/Application/DegreeProgramViewRaw.php index 2729f74..ba26531 100644 --- a/src/Application/DegreeProgramViewRaw.php +++ b/src/Application/DegreeProgramViewRaw.php @@ -5,6 +5,7 @@ namespace Fau\DegreeProgram\Common\Application; use Fau\DegreeProgram\Common\Domain\AdmissionRequirements; +use Fau\DegreeProgram\Common\Domain\CampoKeys; use Fau\DegreeProgram\Common\Domain\Content; use Fau\DegreeProgram\Common\Domain\Degree; use Fau\DegreeProgram\Common\Domain\DegreeProgram; @@ -75,6 +76,7 @@ private function __construct( private MultilingualLink $studentInitiatives, private MultilingualLink $applyNowLink, private MultilingualString $entryText, + private CampoKeys $campoKeys, ) { } @@ -134,6 +136,7 @@ public static function fromDegreeProgram(DegreeProgram $degreeProgram): self $data[DegreeProgram::STUDENT_INITIATIVES], $data[DegreeProgram::APPLY_NOW_LINK], $data[DegreeProgram::ENTRY_TEXT], + $data[DegreeProgram::CAMPO_KEYS], ); } @@ -198,6 +201,7 @@ public static function fromArray(array $data): self ), applyNowLink: MultilingualLink::fromArray($data[DegreeProgram::APPLY_NOW_LINK]), entryText: MultilingualString::fromArray($data[DegreeProgram::ENTRY_TEXT]), + campoKeys: CampoKeys::fromArray($data[DegreeProgram::CAMPO_KEYS]), ); } @@ -261,6 +265,7 @@ public function asArray(): array DegreeProgram::STUDENT_INITIATIVES => $this->studentInitiatives->asArray(), DegreeProgram::APPLY_NOW_LINK => $this->applyNowLink->asArray(), DegreeProgram::ENTRY_TEXT => $this->entryText->asArray(), + DegreeProgram::CAMPO_KEYS => $this->campoKeys->asArray(), ]; } @@ -513,4 +518,9 @@ public function entryText(): MultilingualString { return $this->entryText; } + + public function campoKeys(): CampoKeys + { + return $this->campoKeys; + } } diff --git a/src/Application/DegreeProgramViewTranslated.php b/src/Application/DegreeProgramViewTranslated.php index c49dbc1..06503ae 100644 --- a/src/Application/DegreeProgramViewTranslated.php +++ b/src/Application/DegreeProgramViewTranslated.php @@ -4,6 +4,7 @@ namespace Fau\DegreeProgram\Common\Application; +use Fau\DegreeProgram\Common\Domain\CampoKeys; use Fau\DegreeProgram\Common\Domain\DegreeProgram; use Fau\DegreeProgram\Common\Domain\DegreeProgramId; use Fau\DegreeProgram\Common\Domain\MultilingualString; @@ -21,6 +22,7 @@ * @psalm-import-type LanguageCodes from MultilingualString * @psalm-import-type ImageViewType from ImageView * @psalm-import-type NumberOfStudentsType from NumberOfStudents + * @psalm-import-type CampoKeysMap from CampoKeys * @psalm-type DegreeProgramTranslation = array{ * link: string, * slug: string, @@ -73,6 +75,7 @@ * student_initiatives: LinkType, * apply_now_link: LinkType, * entry_text: string, + * campo_keys: CampoKeysMap * } * @psalm-type DegreeProgramViewTranslatedArrayType = DegreeProgramTranslation & array{ * id: int, @@ -145,6 +148,7 @@ public function __construct( private Link $studentInitiatives, private Link $applyNowLink, private string $entryText, + private CampoKeys $campoKeys, ) { } @@ -209,6 +213,7 @@ public static function empty(int $id, string $languageCode): self studentInitiatives: Link::empty(), applyNowLink: Link::empty(), entryText: '', + campoKeys: CampoKeys::empty(), ); } @@ -277,6 +282,7 @@ public static function fromArray(array $data): self studentInitiatives: Link::fromArray($data[DegreeProgram::STUDENT_INITIATIVES]), applyNowLink: Link::fromArray($data[DegreeProgram::APPLY_NOW_LINK]), entryText: $data[DegreeProgram::ENTRY_TEXT], + campoKeys: CampoKeys::fromArray($data[DegreeProgram::CAMPO_KEYS]), ); if (empty($data[self::TRANSLATIONS])) { @@ -350,6 +356,7 @@ public function asArray(): array DegreeProgram::STUDENT_INITIATIVES => $this->studentInitiatives->asArray(), DegreeProgram::APPLY_NOW_LINK => $this->applyNowLink->asArray(), DegreeProgram::ENTRY_TEXT => $this->entryText, + DegreeProgram::CAMPO_KEYS => $this->campoKeys->asArray(), self::TRANSLATIONS => $this->translationsAsArray(), ]; } @@ -669,4 +676,9 @@ public function entryText(): string { return $this->entryText; } + + public function campoKeys(): CampoKeys + { + return $this->campoKeys; + } } diff --git a/src/Application/Repository/CollectionCriteria.php b/src/Application/Repository/CollectionCriteria.php index b9cc924..53dd383 100644 --- a/src/Application/Repository/CollectionCriteria.php +++ b/src/Application/Repository/CollectionCriteria.php @@ -18,6 +18,7 @@ * include?: array, * search?: string, * order_by: OrderBy, + * his_codes?: array * } */ final class CollectionCriteria @@ -36,6 +37,11 @@ final class CollectionCriteria */ private array $filters = []; + /** + * @var array + */ + private array $hisCodes = []; + /** * @var LanguageCodes|null */ @@ -170,6 +176,17 @@ public function withOrderBy(array $orderBy): self return $instance; } + /** + * @param array $hisCodes + * @return self + */ + public function withHisCodes(array $hisCodes): self + { + $instance = clone $this; + $instance->hisCodes = $hisCodes; + return $instance; + } + /** * @psalm-return SupportedArgs */ @@ -185,4 +202,12 @@ public function filters(): array { return $this->filters; } + + /** + * @return array + */ + public function hisCodes(): array + { + return $this->hisCodes; + } } diff --git a/src/Domain/CampoKeys.php b/src/Domain/CampoKeys.php new file mode 100644 index 0000000..7788684 --- /dev/null +++ b/src/Domain/CampoKeys.php @@ -0,0 +1,90 @@ +, string> + */ +final class CampoKeys +{ + public const SCHEMA = [ + 'type' => 'object', + 'properties' => [ + DegreeProgram::DEGREE => [ + 'type' => 'string', + ], + DegreeProgram::AREA_OF_STUDY => [ + 'type' => 'string', + ], + DegreeProgram::LOCATION => [ + 'type' => 'string', + ], + ], + ]; + + public const SCHEMA_REQUIRED = [ + 'type' => 'object', + 'properties' => [ + DegreeProgram::DEGREE => [ + 'type' => 'string', + ], + DegreeProgram::AREA_OF_STUDY => [ + 'type' => 'string', + ], + DegreeProgram::LOCATION => [ + 'type' => 'string', + ], + ], + ]; + + public const SUPPORTED_CAMPO_KEYS = [ + DegreeProgram::DEGREE, + DegreeProgram::AREA_OF_STUDY, + DegreeProgram::LOCATION, + ]; + + private const HIS_CODE_DELIMITER = '|'; + + private function __construct( + /** + * @var CampoKeysMap $map + */ + private array $map + ) { + } + + public static function empty(): self + { + return new self([]); + } + + /** + * @param CampoKeysMap $map + */ + public static function fromArray(array $map): self + { + return new self($map); + } + + public static function fromHisCode(string $hisCode): self + { + $parts = explode(self::HIS_CODE_DELIMITER, $hisCode); + $map = [ + DegreeProgram::DEGREE => $parts[0] ?? null, + DegreeProgram::AREA_OF_STUDY => $parts[1] ?? null, + DegreeProgram::LOCATION => $parts[6] ?? null, + ]; + + return new self(array_filter($map, fn($value) => !is_null($value))); + } + + /** + * @return CampoKeysMap + */ + public function asArray(): array + { + return $this->map; + } +} diff --git a/src/Domain/DegreeProgram.php b/src/Domain/DegreeProgram.php index 6ed4fc0..e3e7243 100644 --- a/src/Domain/DegreeProgram.php +++ b/src/Domain/DegreeProgram.php @@ -17,6 +17,7 @@ * @psalm-import-type AdmissionRequirementsType from AdmissionRequirements * @psalm-import-type DegreeType from Degree * @psalm-import-type NumberOfStudentsType from NumberOfStudents + * @psalm-import-type CampoKeysMap from CampoKeys * @psalm-type DegreeProgramArrayType = array{ * id: int, * slug: MultilingualStringType, @@ -67,6 +68,7 @@ * student_initiatives: MultilingualLinkType, * apply_now_link: MultilingualLinkType, * entry_text: MultilingualStringType, + * campo_keys: CampoKeysMap, * } */ final class DegreeProgram @@ -122,6 +124,7 @@ final class DegreeProgram public const NOTES_FOR_INTERNATIONAL_APPLICANTS = 'notes_for_international_applicants'; public const STUDENT_INITIATIVES = 'student_initiatives'; public const APPLY_NOW_LINK = 'apply_now_link'; + public const CAMPO_KEYS = 'campo_keys'; private IntegersListChangeset $combinationsChangeset; private IntegersListChangeset $limitedCombinationsChangeset; @@ -315,6 +318,10 @@ public function __construct( * Eingeschränkt Kombinationsmöglichkeiten */ private DegreeProgramIds $limitedCombinations, + /** + * CampoKeys + */ + private CampoKeys $campoKeys, ) { $this->combinationsChangeset = IntegersListChangeset::new( @@ -451,6 +458,7 @@ private function update(array $data): void $this->studentInitiatives = MultilingualLink::fromArray($data[self::STUDENT_INITIATIVES]); $this->applyNowLink = MultilingualLink::fromArray($data[self::APPLY_NOW_LINK]); $this->entryText = MultilingualString::fromArray($data[self::ENTRY_TEXT]); + $this->campoKeys = CampoKeys::fromArray($data[self::CAMPO_KEYS]); $this->combinationsChangeset = $this ->combinationsChangeset @@ -515,6 +523,7 @@ private function update(array $data): void * student_initiatives: MultilingualLink, * apply_now_link: MultilingualLink, * entry_text: MultilingualString, + * campo_keys: CampoKeys, * } * @internal Only for repositories usage * phpcs:disable Inpsyde.CodeQuality.FunctionLength.TooLong @@ -574,6 +583,7 @@ public function asArray(): array self::STUDENT_INITIATIVES => $this->studentInitiatives, self::APPLY_NOW_LINK => $this->applyNowLink, self::ENTRY_TEXT => $this->entryText, + self::CAMPO_KEYS => $this->campoKeys, ]; } diff --git a/src/Infrastructure/Repository/CampoKeysRepository.php b/src/Infrastructure/Repository/CampoKeysRepository.php new file mode 100644 index 0000000..3031a45 --- /dev/null +++ b/src/Infrastructure/Repository/CampoKeysRepository.php @@ -0,0 +1,110 @@ + DegreeProgram::DEGREE, + StudyLocationTaxonomy::KEY => DegreeProgram::LOCATION, + AreaOfStudyTaxonomy::KEY => DegreeProgram::AREA_OF_STUDY, + ]; + + public const CAMPO_KEY_TERM_META_KEY = 'uniquename'; + + public function degreeProgramCampoKeys(DegreeProgramId $degreeProgramId): CampoKeys + { + /** @var WP_Error|array $terms */ + $terms = wp_get_post_terms( + $degreeProgramId->asInt(), + array_keys(self::TAXONOMY_TO_CAMPO_KEY_MAP) + ); + + if ($terms instanceof WP_Error) { + return CampoKeys::empty(); + } + + $map = []; + + foreach ($terms as $term) { + $campoKey = (string) get_term_meta($term->term_id, self::CAMPO_KEY_TERM_META_KEY, true); + + if (empty($campoKey)) { + continue; + } + + $campoKeyType = self::TAXONOMY_TO_CAMPO_KEY_MAP[$term->taxonomy] ?? null; + + if (is_null($campoKeyType)) { + continue; + } + + $map[$campoKeyType] = $campoKey; + } + + return CampoKeys::fromArray($map); + } + + /** + * Return a map of taxonomy keys to terms based on a given HIS code. + * + * @throws RuntimeException + * @return array + */ + public function taxonomyToTermsMapFromCampoKeys(CampoKeys $campoKeys): array + { + $result = []; + + $campoKeys = $campoKeys->asArray(); + + foreach (self::TAXONOMY_TO_CAMPO_KEY_MAP as $taxonomy => $campoKeyType) { + $campoKey = $campoKeys[$campoKeyType] ?? ''; + + if ($campoKey === '') { + continue; + } + + $term = $this->findTermByCampoKey($taxonomy, $campoKey); + + if (! $term instanceof WP_Term) { + throw new RuntimeException('Could not find term for Campo key: ' . $campoKey); + } + + $result[$taxonomy] = $term->term_id; + } + + return $result; + } + + private function findTermByCampoKey(string $taxonomy, string $campoKey): ?WP_Term + { + if ($campoKey === '') { + return null; + } + + /** @var WP_Error|array $terms */ + $terms = get_terms([ + 'taxonomy' => $taxonomy, + 'meta_key' => self::CAMPO_KEY_TERM_META_KEY, + 'meta_value' => $campoKey, + ]); + + if ($terms instanceof WP_Error) { + return null; + } + + return $terms[0] ?? null; + } +} diff --git a/src/Infrastructure/Repository/WordPressDatabaseDegreeProgramRepository.php b/src/Infrastructure/Repository/WordPressDatabaseDegreeProgramRepository.php index 36a08b3..bd3f746 100644 --- a/src/Infrastructure/Repository/WordPressDatabaseDegreeProgramRepository.php +++ b/src/Infrastructure/Repository/WordPressDatabaseDegreeProgramRepository.php @@ -52,6 +52,7 @@ public function __construct( IdGenerator $idGenerator, private EventDispatcherInterface $eventDispatcher, private HtmlDegreeProgramSanitizer $fieldsSanitizer, + private CampoKeysRepository $campoKeysRepository, ) { parent::__construct($idGenerator); @@ -240,6 +241,7 @@ public function getById(DegreeProgramId $degreeProgramId): DegreeProgram $postId, DegreeProgram::LIMITED_COMBINATIONS ), + campoKeys: $this->campoKeysRepository->degreeProgramCampoKeys($degreeProgramId), ); } diff --git a/src/Infrastructure/Repository/WordPressDatabaseDegreeProgramViewRepository.php b/src/Infrastructure/Repository/WordPressDatabaseDegreeProgramViewRepository.php index f1d4c0f..7d32c54 100644 --- a/src/Infrastructure/Repository/WordPressDatabaseDegreeProgramViewRepository.php +++ b/src/Infrastructure/Repository/WordPressDatabaseDegreeProgramViewRepository.php @@ -182,6 +182,7 @@ private function translateDegreeProgram( studentInitiatives: Link::fromMultilingualLink($raw->studentInitiatives(), $languageCode), applyNowLink: Link::fromMultilingualLink($raw->applyNowLink(), $languageCode), entryText: $this->formatContentField($raw->entryText()->asString($languageCode)), + campoKeys: $raw->campoKeys(), ); } diff --git a/src/Infrastructure/Repository/WpQueryArgsBuilder.php b/src/Infrastructure/Repository/WpQueryArgsBuilder.php index bd1d7c6..7a6717f 100644 --- a/src/Infrastructure/Repository/WpQueryArgsBuilder.php +++ b/src/Infrastructure/Repository/WpQueryArgsBuilder.php @@ -17,6 +17,7 @@ use Fau\DegreeProgram\Common\Application\Filter\SubjectGroupFilter; use Fau\DegreeProgram\Common\Application\Filter\TeachingLanguageFilter; use Fau\DegreeProgram\Common\Application\Repository\CollectionCriteria; +use Fau\DegreeProgram\Common\Domain\CampoKeys; use Fau\DegreeProgram\Common\Domain\DegreeProgram; use Fau\DegreeProgram\Common\Domain\MultilingualString; use Fau\DegreeProgram\Common\Infrastructure\Content\PostType\DegreeProgramPostType; @@ -24,6 +25,7 @@ use Fau\DegreeProgram\Common\Infrastructure\Content\Taxonomy\MasterDegreeAdmissionRequirementTaxonomy; use Fau\DegreeProgram\Common\Infrastructure\Content\Taxonomy\TaxonomiesList; use Fau\DegreeProgram\Common\Infrastructure\Content\Taxonomy\TeachingDegreeHigherSemesterAdmissionRequirementTaxonomy; +use RuntimeException; use WP_Term; /** @@ -64,8 +66,10 @@ final class WpQueryArgsBuilder 'date' => 'desc', ]; - public function __construct(private TaxonomiesList $taxonomiesList) - { + public function __construct( + private TaxonomiesList $taxonomiesList, + private CampoKeysRepository $campoKeysRepository, + ) { } public function build(CollectionCriteria $criteria): WpQueryArgs @@ -84,9 +88,47 @@ public function build(CollectionCriteria $criteria): WpQueryArgs $queryArgs = $this->applyFilter($filter, $queryArgs, $criteria->languageCode()); } + foreach ($criteria->hisCodes() as $hisCode) { + $queryArgs = $this->applyHisCode($hisCode, $queryArgs); + } + return $queryArgs; } + public function applyHisCode(string $hisCode, WpQueryArgs $queryArgs): WpQueryArgs + { + $taxQueryItem = [ + 'relation' => 'AND', + ]; + + try { + $taxonomyToTermMapping = $this->campoKeysRepository->taxonomyToTermsMapFromCampoKeys( + CampoKeys::fromHisCode($hisCode) + ); + + if (count($taxonomyToTermMapping) === 0) { + return $queryArgs; + } + + foreach ($taxonomyToTermMapping as $taxonomy => $termId) { + $taxQueryItem[] = [ + 'taxonomy' => $taxonomy, + 'terms' => [ + $termId, + ], + ]; + } + + return $queryArgs->withTaxQueryItem($taxQueryItem); + } catch (RuntimeException) { + /* + * Return an empty result if one or more campo keys in HIS code are not matched to any terms. + * Otherwise invalid HIS codes would be matched to false results. + */ + return $queryArgs->withArg('post__in', [0]); + } + } + private function applyOrderBy( CollectionCriteria $criteria, WpQueryArgs $queryArgs diff --git a/src/Infrastructure/RestApi/TranslatedDegreeProgramController.php b/src/Infrastructure/RestApi/TranslatedDegreeProgramController.php index da3145c..570dcd1 100644 --- a/src/Infrastructure/RestApi/TranslatedDegreeProgramController.php +++ b/src/Infrastructure/RestApi/TranslatedDegreeProgramController.php @@ -676,6 +676,14 @@ public function get_item_schema(): array ), 'type' => 'object', ], + DegreeProgram::CAMPO_KEYS => [ + 'description' => _x( + 'Degree program Campo Keys.', + 'rest_api: schema item description', + 'fau-degree-program' + ), + 'type' => 'object', + ], ]; return $this->schema; diff --git a/src/Infrastructure/Validator/JsonSchemaDegreeProgramDataValidator.php b/src/Infrastructure/Validator/JsonSchemaDegreeProgramDataValidator.php index f9f2e77..83f2a0f 100644 --- a/src/Infrastructure/Validator/JsonSchemaDegreeProgramDataValidator.php +++ b/src/Infrastructure/Validator/JsonSchemaDegreeProgramDataValidator.php @@ -76,6 +76,7 @@ final class JsonSchemaDegreeProgramDataValidator implements DegreeProgramDataVal DegreeProgram::LIMITED_COMBINATIONS, DegreeProgram::NOTES_FOR_INTERNATIONAL_APPLICANTS, DegreeProgram::APPLY_NOW_LINK, + DegreeProgram::CAMPO_KEYS, ]; /** diff --git a/tests/resources/fixtures/degree_program.json b/tests/resources/fixtures/degree_program.json index 0d6446f..a92f942 100644 --- a/tests/resources/fixtures/degree_program.json +++ b/tests/resources/fixtures/degree_program.json @@ -653,5 +653,10 @@ "id": "post_meta:25:entry_text", "de": "Einstiegtext (werbend)", "en": "Entry text (promotional)" + }, + "campo_keys": { + "degree": "185", + "area_of_study": "85", + "location": "E" } } diff --git a/tests/src/FixtureDegreeProgramDataProviderTrait.php b/tests/src/FixtureDegreeProgramDataProviderTrait.php index 23e5b35..c5cb2d5 100644 --- a/tests/src/FixtureDegreeProgramDataProviderTrait.php +++ b/tests/src/FixtureDegreeProgramDataProviderTrait.php @@ -6,6 +6,7 @@ use Fau\DegreeProgram\Common\Domain\AdmissionRequirement; use Fau\DegreeProgram\Common\Domain\AdmissionRequirements; +use Fau\DegreeProgram\Common\Domain\CampoKeys; use Fau\DegreeProgram\Common\Domain\Content; use Fau\DegreeProgram\Common\Domain\ContentItem; use Fau\DegreeProgram\Common\Domain\Degree; @@ -110,6 +111,7 @@ public function createEmptyDegreeProgram(int $id): DegreeProgram applyNowLink: MultilingualLink::empty(), combinations: DegreeProgramIds::new(), limitedCombinations: DegreeProgramIds::new(), + campoKeys: CampoKeys::empty(), ); } } diff --git a/tests/src/Repository/StubDegreeProgramRepository.php b/tests/src/Repository/StubDegreeProgramRepository.php index f17d86b..cc82796 100644 --- a/tests/src/Repository/StubDegreeProgramRepository.php +++ b/tests/src/Repository/StubDegreeProgramRepository.php @@ -145,6 +145,7 @@ private function translateDegreeProgram( notesForInternationalApplicants: Link::fromMultilingualLink($raw->notesForInternationalApplicants(), $languageCode), applyNowLink: Link::fromMultilingualLink($raw->applyNowLink(), $languageCode), entryText: $raw->entryText()->asString($languageCode), + campoKeys: $raw->campoKeys()->asArray(), ); }