From 4f782b593ffc9a119892b58d36703154145cd585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Arroyo?= Date: Thu, 10 Mar 2022 15:59:18 +0100 Subject: [PATCH 1/7] feat: PoC for object mapper --- .../Annotation/RdfAttributeMapping.php | 65 ++++++++++ .../RdfResourceAttributeMapping.php | 59 +++++++++ .../Annotation/RdfResourceAttributeType.php | 27 ++++ .../Hydrator/ResourceHydrator.php | 97 ++++++++++++++ .../rdfObjectMapper/RdfObjectMapper.php | 94 ++++++++++++++ .../TargetTypes/UserSettingsMappedType.php | 119 ++++++++++++++++++ .../implementation/UserSettingsService.php | 12 ++ 7 files changed, 473 insertions(+) create mode 100644 models/classes/rdfObjectMapper/Annotation/RdfAttributeMapping.php create mode 100644 models/classes/rdfObjectMapper/Annotation/RdfResourceAttributeMapping.php create mode 100644 models/classes/rdfObjectMapper/Annotation/RdfResourceAttributeType.php create mode 100644 models/classes/rdfObjectMapper/Hydrator/ResourceHydrator.php create mode 100644 models/classes/rdfObjectMapper/RdfObjectMapper.php create mode 100644 models/classes/rdfObjectMapper/TargetTypes/UserSettingsMappedType.php diff --git a/models/classes/rdfObjectMapper/Annotation/RdfAttributeMapping.php b/models/classes/rdfObjectMapper/Annotation/RdfAttributeMapping.php new file mode 100644 index 00000000000..6d72de201bc --- /dev/null +++ b/models/classes/rdfObjectMapper/Annotation/RdfAttributeMapping.php @@ -0,0 +1,65 @@ +propertyUri = $propertyUri; + $this->attributeType = $attributeType; + $this->mappedField = $mappedField; + }*/ + + /*public function __construct() //($data, $b, $c) + { + //$this->attributeType = $attributeType; + + //echo "Called RdfAttributeMapping ctor with ".var_export($data,true); + }*/ + + public function hydrate( + \ReflectionProperty $property, + \core_kernel_classes_Resource $src, + object &$targetObject + ): void + { + echo __CLASS__ . " should map a value to the property
\n"; + + $values = $src->getPropertyValues( + new \core_kernel_classes_Property($this->propertyUri) + ); + + if(count($values) == 0) { + echo "No value to map"; + return; + } + + if(count($values) > 1) { + echo "too many values to map"; + return; + } + + if(count($values) == 1) { + $value = current($values); + echo "Mapping value {$value} into {$property->getName()}
"; + + $property->setAccessible(true); + $property->setValue($targetObject, $value); + } + } +} diff --git a/models/classes/rdfObjectMapper/Annotation/RdfResourceAttributeMapping.php b/models/classes/rdfObjectMapper/Annotation/RdfResourceAttributeMapping.php new file mode 100644 index 00000000000..c131889976f --- /dev/null +++ b/models/classes/rdfObjectMapper/Annotation/RdfResourceAttributeMapping.php @@ -0,0 +1,59 @@ +attributeType = $attributeType; + + //echo "Called RdfResourceAttributeMapping ctor with ".var_export($data,true); + }*/ + + public function hydrate( + \ReflectionProperty $property, + \core_kernel_classes_Resource $src, + object &$targetObject + ): void + { + echo __CLASS__ . + " should map a (direct) value from". + " the resource class to the property
\n"; + + $value = null; + switch ($this->type) + { + case RdfResourceAttributeType::LABEL: + $value = $src->getLabel(); + break; + case RdfResourceAttributeType::COMMENT: + $value = $src->getComment(); + break; + case RdfResourceAttributeType::URI: + $value = $src->getUri(); + break; + default: + throw new \LogicException( + "Unknown ".__CLASS__."::type value: ". + $this->type + ); + } + + echo "Mapping value {$value} into {$property->getName()}
"; + + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + // Not needed starting PHP 8.1 (it has become a no-op since then) + $property->setAccessible(true); + } + + $property->setValue($targetObject, $value); + } +} diff --git a/models/classes/rdfObjectMapper/Annotation/RdfResourceAttributeType.php b/models/classes/rdfObjectMapper/Annotation/RdfResourceAttributeType.php new file mode 100644 index 00000000000..cbfe0f23d67 --- /dev/null +++ b/models/classes/rdfObjectMapper/Annotation/RdfResourceAttributeType.php @@ -0,0 +1,27 @@ +reader = new AnnotationReader(); + + // Too bad this pollutes the global annotation reader state + AnnotationRegistry::loadAnnotationClass(RdfResourceAttributeMapping::class); + AnnotationRegistry::loadAnnotationClass(RdfAttributeMapping::class); + } + + public function hydrateInstance( + ReflectionClass $reflector, + core_kernel_classes_Resource $src, + object &$targetObject + ) + { + foreach($reflector->getProperties() as $property) { + echo $property->getName()."
\n"; + + $propertyAnnotations = $this->reader->getPropertyAnnotations( + $property + ); + + echo "Property {$property->getName()} annotations
\n"; + + foreach ($propertyAnnotations as $annotation) + { + // @todo We may delegate the initialization to the annotation + // class itself (i.e. pass a ref to the attribute and the + // value)? + if($annotation instanceof RdfResourceAttributeMapping) + { + echo "-> RdfResourceAttributeMapping
\n"; + $annotation->hydrate($property, $src, $targetObject); + continue; + } + + if($annotation instanceof RdfAttributeMapping) + { + echo "-> RdfAttributeMapping
\n"; + $annotation->hydrate($property, $src, $targetObject); + continue; + } + + throw new Exception( + "Unknown class property type: " . get_class($annotation) + ); + } + + echo "
\n", "
\n", "
\n"; + } + + } +} \ No newline at end of file diff --git a/models/classes/rdfObjectMapper/RdfObjectMapper.php b/models/classes/rdfObjectMapper/RdfObjectMapper.php new file mode 100644 index 00000000000..8c80c6f5d8d --- /dev/null +++ b/models/classes/rdfObjectMapper/RdfObjectMapper.php @@ -0,0 +1,94 @@ +hydrator = new ResourceHydrator(); + } + + public function mapResource( + core_kernel_classes_Resource $resource, + string $targetClass + ): object + { + $reflector = $this->reflect($targetClass); + $instance = $reflector->newInstanceWithoutConstructor(); + + $this->hydrator->hydrateInstance($reflector, $resource, $instance); + + echo "instance hydrated:
"; + echo "
".var_export($instance, true)."
"; + + $this->callConstructorIfPresent($reflector, $instance); + + return $instance; + } + + /** + * @throws ReflectionException + */ + private function reflect(string $targetClass): ReflectionClass + { + return new ReflectionClass($targetClass); + } + + private function callConstructorIfPresent( + ReflectionClass $reflector, + object $instance + ) + { + if($reflector->getConstructor() != null) + { + $closure = $reflector->getConstructor()->getClosure(); + $closure->call($instance); + } + } +} diff --git a/models/classes/rdfObjectMapper/TargetTypes/UserSettingsMappedType.php b/models/classes/rdfObjectMapper/TargetTypes/UserSettingsMappedType.php new file mode 100644 index 00000000000..323ff9c8839 --- /dev/null +++ b/models/classes/rdfObjectMapper/TargetTypes/UserSettingsMappedType.php @@ -0,0 +1,119 @@ +getRange()) + // - 'resource' (complete resource instance) + // - 'uri' (resource URI as string) + 'uri' + )]*/ + + /** @RdfAttributeMapping( + * propertyUri = GenerisRdf::PROPERTY_USER_UILG, + * attributeType = "resource", + * mappedField = "uri") + */ + private /*?string*/ $uiLanguageCode; + + /** + * @RdfAttributeMapping( + * propertyUri = GenerisRdf::PROPERTY_USER_DEFLG, + * attributeType = "resource", + * mappedField = "uri") + */ + private /*?string*/ $dataLanguage; + + /** + * property returned as a core_kernel_classes_Literal instance, + * will be converted to string + * @todo Use RdfResourceAttributeType (or a new class) constants for + * attributeType + * @RdfAttributeMapping( + * GenerisRdf::PROPERTY_USER_TIMEZONE, attributeType = "literal") + */ + private /*string*/ $timezone; + + public function getUri(): string + { + return $this->userUri; + } + + public function getLabel(): string + { + return $this->userLabel; + } + + public function getComment(): string + { + return $this->userComment; + } + + public function getUILanguageCode(): ?string + { + return $this->uiLanguageCode; + } + + public function getDataLanguageCode(): ?string + { + return $this->dataLanguage; + } + + public function getTimezone(): string + { + return $this->timezone; + } +} diff --git a/models/classes/user/implementation/UserSettingsService.php b/models/classes/user/implementation/UserSettingsService.php index 2424a8ece74..7a7e535a0b2 100644 --- a/models/classes/user/implementation/UserSettingsService.php +++ b/models/classes/user/implementation/UserSettingsService.php @@ -25,10 +25,16 @@ use oat\generis\model\data\Ontology; use oat\generis\model\GenerisRdf; use oat\oatbox\user\UserTimezoneServiceInterface; +use oat\tao\model\RdfObjectMapper\RdfObjectMapper; +use oat\tao\model\RdfObjectMapper\TargetTypes\UserSettingsMappedType; use oat\tao\model\user\UserSettingsInterface; use oat\tao\model\user\UserSettingsServiceInterface; use core_kernel_classes_Resource; +// For some reason composer is not loading these +include __DIR__.'/../../rdfObjectMapper/RdfObjectMapper.php'; +include __DIR__.'/../../rdfObjectMapper/TargetTypes/UserSettingsMappedType.php'; + class UserSettingsService implements UserSettingsServiceInterface { /** @var Ontology */ @@ -45,6 +51,12 @@ public function __construct(UserTimezoneServiceInterface $userTimezoneService, O public function get(core_kernel_classes_Resource $user): UserSettingsInterface { + echo "Mapping resource with URI {$user->getUri()}

\n\n"; + + $mapper = new RdfObjectMapper(); + $mapper->mapResource($user, UserSettingsMappedType::class); + + $props = $user->getPropertiesValues( [ $this->ontology->getProperty(GenerisRdf::PROPERTY_USER_UILG), From a94388e65c3d4bb791bbd553803d3f5125d5e5d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Arroyo?= Date: Thu, 10 Mar 2022 18:01:11 +0100 Subject: [PATCH 2/7] chore: Cleanup --- .../Annotation/RdfAttributeMapping.php | 102 ++++++++++++++++++ .../RdfResourceAttributeMapping.php | 90 ++++++++++++++++ .../Annotation/RdfResourceAttributeType.php | 1 + .../Hydrator/ResourceHydrator.php | 37 +++---- .../RdfObjectMapper.php | 54 ++++++---- .../TargetTypes/UserSettingsMappedType.php | 12 +-- .../Annotation/RdfAttributeMapping.php | 65 ----------- .../RdfResourceAttributeMapping.php | 59 ---------- .../implementation/UserSettingsService.php | 26 +++-- 9 files changed, 265 insertions(+), 181 deletions(-) create mode 100644 models/classes/RdfObjectMapper/Annotation/RdfAttributeMapping.php create mode 100644 models/classes/RdfObjectMapper/Annotation/RdfResourceAttributeMapping.php rename models/classes/{rdfObjectMapper => RdfObjectMapper}/Annotation/RdfResourceAttributeType.php (99%) rename models/classes/{rdfObjectMapper => RdfObjectMapper}/Hydrator/ResourceHydrator.php (69%) rename models/classes/{rdfObjectMapper => RdfObjectMapper}/RdfObjectMapper.php (59%) rename models/classes/{rdfObjectMapper => RdfObjectMapper}/TargetTypes/UserSettingsMappedType.php (91%) delete mode 100644 models/classes/rdfObjectMapper/Annotation/RdfAttributeMapping.php delete mode 100644 models/classes/rdfObjectMapper/Annotation/RdfResourceAttributeMapping.php diff --git a/models/classes/RdfObjectMapper/Annotation/RdfAttributeMapping.php b/models/classes/RdfObjectMapper/Annotation/RdfAttributeMapping.php new file mode 100644 index 00000000000..741448e6471 --- /dev/null +++ b/models/classes/RdfObjectMapper/Annotation/RdfAttributeMapping.php @@ -0,0 +1,102 @@ +propertyUri = $propertyUri; + $this->attributeType = $attributeType; + $this->mappedField = $mappedField; + }*/ + + /*public function __construct() //($data, $b, $c) + { + //$this->attributeType = $attributeType; + + //echo "Called RdfAttributeMapping ctor with ".var_export($data,true); + }*/ + + // You may see keeping hydrate() here in the annotation class as a good + // thing (as in "higher cohesion", keeping the value initialization in the + // annotation implementation) or bad thing (as in "higher coupling", + // preventing having more than one behaviour (implementation) for a given + // annotation). + // + // Maybe we'll want to allow overriding the behaviour for a given annotation + // somehow in the future (for example, from extensions)). + // + public function hydrate( + LoggerInterface $logger, + ReflectionProperty $property, + core_kernel_classes_Resource $src, + object $targetObject + ): void + { + $logger->debug(__CLASS__ . " maps a value to the property"); + + $values = $src->getPropertyValues( + new \core_kernel_classes_Property($this->propertyUri) + ); + + if(count($values) == 0) { + $logger->warning(__CLASS__ . " no value to map"); + return; + } + + if(count($values) > 1) { + $logger->warning(__CLASS__ . "too many values to map"); + return; + } + + if(count($values) == 1) { + $value = current($values); + $logger->info( + sprintf("%s Mapping value %s into %s", + __CLASS__, + $value, + $property->getName() + ) + ); + + $property->setAccessible(true); + $property->setValue($targetObject, $value); + } + } +} diff --git a/models/classes/RdfObjectMapper/Annotation/RdfResourceAttributeMapping.php b/models/classes/RdfObjectMapper/Annotation/RdfResourceAttributeMapping.php new file mode 100644 index 00000000000..b245f43ff41 --- /dev/null +++ b/models/classes/RdfObjectMapper/Annotation/RdfResourceAttributeMapping.php @@ -0,0 +1,90 @@ +attributeType = $attributeType; + + //echo "Called RdfResourceAttributeMapping ctor with ".var_export($data,true); + }*/ + + public function hydrate( + LoggerInterface $logger, + ReflectionProperty $property, + core_kernel_classes_Resource $src, + object $targetObject + ): void + { + $logger->debug( + __CLASS__ . + ' maps a (direct) value from the resource class to the property' + ); + + switch ($this->type) + { + case RdfResourceAttributeType::LABEL: + $value = $src->getLabel(); + break; + case RdfResourceAttributeType::COMMENT: + $value = $src->getComment(); + break; + case RdfResourceAttributeType::URI: + $value = $src->getUri(); + break; + default: + throw new LogicException( + "Unknown ".__CLASS__."::type value: ". + $this->type + ); + } + + $logger->info( + sprintf("%s Mapping value %s into %s", + __CLASS__, + $value, + $property->getName() + ) + ); + + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + // Not needed starting PHP 8.1 (it has become a no-op since then) + $property->setAccessible(true); + } + + $property->setValue($targetObject, $value); + } +} diff --git a/models/classes/rdfObjectMapper/Annotation/RdfResourceAttributeType.php b/models/classes/RdfObjectMapper/Annotation/RdfResourceAttributeType.php similarity index 99% rename from models/classes/rdfObjectMapper/Annotation/RdfResourceAttributeType.php rename to models/classes/RdfObjectMapper/Annotation/RdfResourceAttributeType.php index cbfe0f23d67..c4f04adf2e5 100644 --- a/models/classes/rdfObjectMapper/Annotation/RdfResourceAttributeType.php +++ b/models/classes/RdfObjectMapper/Annotation/RdfResourceAttributeType.php @@ -1,4 +1,5 @@ logger = $logger; $this->reader = new AnnotationReader(); // Too bad this pollutes the global annotation reader state @@ -58,30 +57,27 @@ public function hydrateInstance( ) { foreach($reflector->getProperties() as $property) { - echo $property->getName()."
\n"; + $this->logger->info("Handling property: {$property->getName()}"); $propertyAnnotations = $this->reader->getPropertyAnnotations( $property ); - echo "Property {$property->getName()} annotations
\n"; + $this->logger->debug("Property {$property->getName()} annotations"); foreach ($propertyAnnotations as $annotation) { - // @todo We may delegate the initialization to the annotation - // class itself (i.e. pass a ref to the attribute and the - // value)? if($annotation instanceof RdfResourceAttributeMapping) { - echo "-> RdfResourceAttributeMapping
\n"; - $annotation->hydrate($property, $src, $targetObject); + $this->logger->debug('-> Using RdfResourceAttributeMapping'); + $annotation->hydrate($this->logger, $property, $src, $targetObject); continue; } if($annotation instanceof RdfAttributeMapping) { - echo "-> RdfAttributeMapping
\n"; - $annotation->hydrate($property, $src, $targetObject); + $this->logger->debug('-> Using RdfAttributeMapping'); + $annotation->hydrate($this->logger, $property, $src, $targetObject); continue; } @@ -89,9 +85,6 @@ public function hydrateInstance( "Unknown class property type: " . get_class($annotation) ); } - - echo "
\n", "
\n", "
\n"; } - } -} \ No newline at end of file +} diff --git a/models/classes/rdfObjectMapper/RdfObjectMapper.php b/models/classes/RdfObjectMapper/RdfObjectMapper.php similarity index 59% rename from models/classes/rdfObjectMapper/RdfObjectMapper.php rename to models/classes/RdfObjectMapper/RdfObjectMapper.php index 8c80c6f5d8d..c8c99b8a9d3 100644 --- a/models/classes/rdfObjectMapper/RdfObjectMapper.php +++ b/models/classes/RdfObjectMapper/RdfObjectMapper.php @@ -20,38 +20,57 @@ namespace oat\tao\model\RdfObjectMapper; +use oat\tao\model\RdfObjectMapper\Hydrator\ResourceHydrator; use oat\tao\model\RdfObjectMapper\TargetTypes\RdfResourceAttributeMapping; -use oat\tao\model\RdfObjectMapper\TargetTypes\ResourceHydrator; +use Psr\Log\LoggerInterface; use RdfAttributeMapping; use core_kernel_classes_Resource; use ReflectionClass; use ReflectionException; +// right not this is needed (it seems they are not autoloaded for some reason) require_once __DIR__ . '/Annotation/RdfAttributeMapping.php'; require_once __DIR__ . '/Annotation/RdfResourceAttributeMapping.php'; require_once __DIR__ . '/Hydrator/ResourceHydrator.php'; -// taoDockerize uses PHP 7.2 and Generis is supporting "php": "^7.1": -// we cannot use native annotations. -// -// However, Generis explicitly depends on doctrine/annotations ~1.6.0, -// we may reimplement this using Doctrine annotations instead. -// -// Maybe this should be in Generis instead? -// @todo The object mapper may be an interface with right now a single -// implementation that uses PHPDoc annotations. Maybe the object mapper -// can just use delegate the mapping to a "child" mapper with something -// like a chain of responsibility, so we can have a mapper based on -// Doctrine annotations while also having the possibility to implement a -// different one using PHP native annotations in the future. +/** + * As Generis explicitly depends on doctrine/annotations ~1.6.0, the current + * implementation uses Doctrine annotations instead without needing additional + * deps. + * + * taoDockerize uses PHP 7.2 and Generis is supporting "php": "^7.1": We cannot + * use native PHP annotations (yet?). + * + * @todo Maybe this should be in Generis instead? + * + * @todo We may provide the object mapper as an interface instead, and provide a + * single implementation that uses PHPDoc annotations. + * + * @todo Another possibility would be to have a "root" object mapper that just + * delegates the mapping to a "child" mapper class (a chain of + * responsibility), so we can have a mapper based on Doctrine annotations + * while also having the possibility to implement a different one using + * PHP native annotations in the future (without needing to convert + * existing classes using Doctrine annotations to the PHP syntax all at + * once). + * + * @todo May be worth considering how this fits with other architectural patterns + * currently under discussion. + * + * @todo Right now only reading RDF data has been considered + */ class RdfObjectMapper { /** @var ResourceHydrator */ private $hydrator; - public function __construct() + /** @var LoggerInterface */ + private $logger; + + public function __construct(LoggerInterface $logger) { - $this->hydrator = new ResourceHydrator(); + $this->logger = $logger; + $this->hydrator = new ResourceHydrator($logger); } public function mapResource( @@ -64,9 +83,6 @@ public function mapResource( $this->hydrator->hydrateInstance($reflector, $resource, $instance); - echo "instance hydrated:
"; - echo "
".var_export($instance, true)."
"; - $this->callConstructorIfPresent($reflector, $instance); return $instance; diff --git a/models/classes/rdfObjectMapper/TargetTypes/UserSettingsMappedType.php b/models/classes/RdfObjectMapper/TargetTypes/UserSettingsMappedType.php similarity index 91% rename from models/classes/rdfObjectMapper/TargetTypes/UserSettingsMappedType.php rename to models/classes/RdfObjectMapper/TargetTypes/UserSettingsMappedType.php index 323ff9c8839..ed5c57e63b8 100644 --- a/models/classes/rdfObjectMapper/TargetTypes/UserSettingsMappedType.php +++ b/models/classes/RdfObjectMapper/TargetTypes/UserSettingsMappedType.php @@ -23,17 +23,16 @@ use oat\generis\model\GenerisRdf; use oat\tao\model\user\UserSettingsInterface; -require_once __DIR__ . '/../Annotation/RdfAttributeMapping.php'; -require_once __DIR__ . '/../Annotation/RdfResourceAttributeMapping.php'; -require_once __DIR__ . '/../Annotation/RdfResourceAttributeType.php'; - use oat\tao\model\RdfObjectMapper\Annotation\RdfAttributeMapping; use oat\tao\model\RdfObjectMapper\Annotation\RdfResourceAttributeMapping; use oat\tao\model\RdfObjectMapper\Annotation\RdfResourceAttributeType; /** - * Example object type with RDF mapping annotations + * Example object type with RDF mapping annotations. + * + * This would be "userland code" for the mapper (i.e. objects using annotations + * are to be located in other models/namespaces (tao/models/classes/user etc) */ class UserSettingsMappedType implements UserSettingsInterface { @@ -83,7 +82,8 @@ class UserSettingsMappedType implements UserSettingsInterface * @todo Use RdfResourceAttributeType (or a new class) constants for * attributeType * @RdfAttributeMapping( - * GenerisRdf::PROPERTY_USER_TIMEZONE, attributeType = "literal") + * GenerisRdf::PROPERTY_USER_TIMEZONE, + * attributeType = "literal") */ private /*string*/ $timezone; diff --git a/models/classes/rdfObjectMapper/Annotation/RdfAttributeMapping.php b/models/classes/rdfObjectMapper/Annotation/RdfAttributeMapping.php deleted file mode 100644 index 6d72de201bc..00000000000 --- a/models/classes/rdfObjectMapper/Annotation/RdfAttributeMapping.php +++ /dev/null @@ -1,65 +0,0 @@ -propertyUri = $propertyUri; - $this->attributeType = $attributeType; - $this->mappedField = $mappedField; - }*/ - - /*public function __construct() //($data, $b, $c) - { - //$this->attributeType = $attributeType; - - //echo "Called RdfAttributeMapping ctor with ".var_export($data,true); - }*/ - - public function hydrate( - \ReflectionProperty $property, - \core_kernel_classes_Resource $src, - object &$targetObject - ): void - { - echo __CLASS__ . " should map a value to the property
\n"; - - $values = $src->getPropertyValues( - new \core_kernel_classes_Property($this->propertyUri) - ); - - if(count($values) == 0) { - echo "No value to map"; - return; - } - - if(count($values) > 1) { - echo "too many values to map"; - return; - } - - if(count($values) == 1) { - $value = current($values); - echo "Mapping value {$value} into {$property->getName()}
"; - - $property->setAccessible(true); - $property->setValue($targetObject, $value); - } - } -} diff --git a/models/classes/rdfObjectMapper/Annotation/RdfResourceAttributeMapping.php b/models/classes/rdfObjectMapper/Annotation/RdfResourceAttributeMapping.php deleted file mode 100644 index c131889976f..00000000000 --- a/models/classes/rdfObjectMapper/Annotation/RdfResourceAttributeMapping.php +++ /dev/null @@ -1,59 +0,0 @@ -attributeType = $attributeType; - - //echo "Called RdfResourceAttributeMapping ctor with ".var_export($data,true); - }*/ - - public function hydrate( - \ReflectionProperty $property, - \core_kernel_classes_Resource $src, - object &$targetObject - ): void - { - echo __CLASS__ . - " should map a (direct) value from". - " the resource class to the property
\n"; - - $value = null; - switch ($this->type) - { - case RdfResourceAttributeType::LABEL: - $value = $src->getLabel(); - break; - case RdfResourceAttributeType::COMMENT: - $value = $src->getComment(); - break; - case RdfResourceAttributeType::URI: - $value = $src->getUri(); - break; - default: - throw new \LogicException( - "Unknown ".__CLASS__."::type value: ". - $this->type - ); - } - - echo "Mapping value {$value} into {$property->getName()}
"; - - if (version_compare(PHP_VERSION, '8.1.0', '<')) { - // Not needed starting PHP 8.1 (it has become a no-op since then) - $property->setAccessible(true); - } - - $property->setValue($targetObject, $value); - } -} diff --git a/models/classes/user/implementation/UserSettingsService.php b/models/classes/user/implementation/UserSettingsService.php index 7a7e535a0b2..aa9616a262a 100644 --- a/models/classes/user/implementation/UserSettingsService.php +++ b/models/classes/user/implementation/UserSettingsService.php @@ -24,39 +24,45 @@ use oat\generis\model\data\Ontology; use oat\generis\model\GenerisRdf; +use oat\oatbox\log\LoggerAwareTrait; use oat\oatbox\user\UserTimezoneServiceInterface; use oat\tao\model\RdfObjectMapper\RdfObjectMapper; use oat\tao\model\RdfObjectMapper\TargetTypes\UserSettingsMappedType; use oat\tao\model\user\UserSettingsInterface; use oat\tao\model\user\UserSettingsServiceInterface; use core_kernel_classes_Resource; +use Psr\Log\LoggerAwareInterface; -// For some reason composer is not loading these -include __DIR__.'/../../rdfObjectMapper/RdfObjectMapper.php'; -include __DIR__.'/../../rdfObjectMapper/TargetTypes/UserSettingsMappedType.php'; - -class UserSettingsService implements UserSettingsServiceInterface +class UserSettingsService implements UserSettingsServiceInterface, LoggerAwareInterface { + use LoggerAwareTrait; + /** @var Ontology */ private $ontology; /** @var string */ private $defaultTimeZone; + private $objectMapper; + public function __construct(UserTimezoneServiceInterface $userTimezoneService, Ontology $ontology) { $this->defaultTimeZone = $userTimezoneService->getDefaultTimezone(); $this->ontology = $ontology; + + // @fixme Inject the mapper + $this->objectMapper = new RdfObjectMapper( + $this->getLogger() + ); } public function get(core_kernel_classes_Resource $user): UserSettingsInterface { - echo "Mapping resource with URI {$user->getUri()}

\n\n"; - - $mapper = new RdfObjectMapper(); - $mapper->mapResource($user, UserSettingsMappedType::class); - + return $this->objectMapper->mapResource($user, UserSettingsMappedType::class); + } + public function previousGetImplementation(core_kernel_classes_Resource $user): UserSettingsInterface + { $props = $user->getPropertiesValues( [ $this->ontology->getProperty(GenerisRdf::PROPERTY_USER_UILG), From bf481dd7f1f3e21297e04d9de4e79ad8b26131bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Arroyo?= Date: Fri, 11 Mar 2022 16:54:54 +0100 Subject: [PATCH 3/7] refactor: Clean things up a little bit --- manifest.php | 2 + .../Contract/RdfObjectMapperInterface.php | 31 +++++++++++ .../{ => Service}/RdfObjectMapper.php | 16 +++--- .../ResourceHydrator.php | 3 +- .../RdfObjectMapperServiceProvider.php | 54 +++++++++++++++++++ .../user/UserSettingsServiceProvider.php | 2 + .../implementation/UserSettingsService.php | 15 +++--- 7 files changed, 106 insertions(+), 17 deletions(-) create mode 100644 models/classes/RdfObjectMapper/Contract/RdfObjectMapperInterface.php rename models/classes/RdfObjectMapper/{ => Service}/RdfObjectMapper.php (87%) rename models/classes/RdfObjectMapper/{Hydrator => Service}/ResourceHydrator.php (96%) create mode 100644 models/classes/RdfObjectMapper/ServiceProvider/RdfObjectMapperServiceProvider.php diff --git a/manifest.php b/manifest.php index 1c12cb82dae..d7c1c550968 100755 --- a/manifest.php +++ b/manifest.php @@ -30,6 +30,7 @@ use oat\tao\model\import\ServiceProvider\ImportServiceProvider; use oat\tao\model\metadata\ServiceProvider\MetadataServiceProvider; use oat\tao\model\Observer\ServiceProvider\ObserverServiceProvider; +use oat\tao\model\RdfObjectMapper\ServiceProvider\RdfObjectMapperServiceProvider; use oat\tao\model\resources\ResourcesServiceProvider; use oat\tao\model\featureFlag\FeatureFlagServiceProvider; use oat\tao\helpers\form\ServiceProvider\FormServiceProvider; @@ -297,5 +298,6 @@ AccessControlServiceProvider::class, MetadataServiceProvider::class, ObserverServiceProvider::class, + RdfObjectMapperServiceProvider::class, ], ]; diff --git a/models/classes/RdfObjectMapper/Contract/RdfObjectMapperInterface.php b/models/classes/RdfObjectMapper/Contract/RdfObjectMapperInterface.php new file mode 100644 index 00000000000..317776d15e6 --- /dev/null +++ b/models/classes/RdfObjectMapper/Contract/RdfObjectMapperInterface.php @@ -0,0 +1,31 @@ +logger = $logger; - $this->hydrator = new ResourceHydrator($logger); + $this->hydrator = $hydrator; } public function mapResource( diff --git a/models/classes/RdfObjectMapper/Hydrator/ResourceHydrator.php b/models/classes/RdfObjectMapper/Service/ResourceHydrator.php similarity index 96% rename from models/classes/RdfObjectMapper/Hydrator/ResourceHydrator.php rename to models/classes/RdfObjectMapper/Service/ResourceHydrator.php index 115d6aeae10..6cea1efe4c7 100644 --- a/models/classes/RdfObjectMapper/Hydrator/ResourceHydrator.php +++ b/models/classes/RdfObjectMapper/Service/ResourceHydrator.php @@ -18,8 +18,7 @@ * Copyright (c) 2022 (original work) Open Assessment Technologies SA. */ -// @todo We may wan to have this in a "Service" namespace instead -namespace oat\tao\model\RdfObjectMapper\Hydrator; +namespace oat\tao\model\RdfObjectMapper\Service; use oat\tao\model\RdfObjectMapper\Annotation\RdfAttributeMapping; use oat\tao\model\RdfObjectMapper\Annotation\RdfResourceAttributeMapping; diff --git a/models/classes/RdfObjectMapper/ServiceProvider/RdfObjectMapperServiceProvider.php b/models/classes/RdfObjectMapper/ServiceProvider/RdfObjectMapperServiceProvider.php new file mode 100644 index 00000000000..6b3fbe052ab --- /dev/null +++ b/models/classes/RdfObjectMapper/ServiceProvider/RdfObjectMapperServiceProvider.php @@ -0,0 +1,54 @@ +services(); + + $services + ->set(ResourceHydrator::class, ResourceHydrator::class) + ->args( + [ + service(LoggerService::SERVICE_ID), + ] + ); + + $services + ->set(RdfObjectMapper::class, RdfObjectMapper::class) + ->args( + [ + service(LoggerService::SERVICE_ID), + service(ResourceHydrator::class), + ] + ) + ->public(); + } +} diff --git a/models/classes/user/UserSettingsServiceProvider.php b/models/classes/user/UserSettingsServiceProvider.php index 87aae5eb132..f23612dfcb5 100644 --- a/models/classes/user/UserSettingsServiceProvider.php +++ b/models/classes/user/UserSettingsServiceProvider.php @@ -25,6 +25,7 @@ use oat\generis\model\data\Ontology; use oat\generis\model\DependencyInjection\ContainerServiceProviderInterface; use oat\oatbox\user\UserTimezoneServiceInterface; +use oat\tao\model\RdfObjectMapper\Service\RdfObjectMapper; use oat\tao\model\user\implementation\UserSettingsService; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use tao_models_classes_LanguageService; @@ -44,6 +45,7 @@ public function __invoke(ContainerConfigurator $configurator): void [ service(UserTimezoneServiceInterface::SERVICE_ID), service(Ontology::SERVICE_ID), + service(RdfObjectMapper::class) ] ); diff --git a/models/classes/user/implementation/UserSettingsService.php b/models/classes/user/implementation/UserSettingsService.php index aa9616a262a..9b994b5208f 100644 --- a/models/classes/user/implementation/UserSettingsService.php +++ b/models/classes/user/implementation/UserSettingsService.php @@ -26,6 +26,7 @@ use oat\generis\model\GenerisRdf; use oat\oatbox\log\LoggerAwareTrait; use oat\oatbox\user\UserTimezoneServiceInterface; +use oat\tao\model\RdfObjectMapper\Contract\RdfObjectMapperInterface; use oat\tao\model\RdfObjectMapper\RdfObjectMapper; use oat\tao\model\RdfObjectMapper\TargetTypes\UserSettingsMappedType; use oat\tao\model\user\UserSettingsInterface; @@ -43,17 +44,17 @@ class UserSettingsService implements UserSettingsServiceInterface, LoggerAwareIn /** @var string */ private $defaultTimeZone; + /** @var RdfObjectMapperInterface */ private $objectMapper; - public function __construct(UserTimezoneServiceInterface $userTimezoneService, Ontology $ontology) - { + public function __construct( + UserTimezoneServiceInterface $userTimezoneService, + Ontology $ontology, + RdfObjectMapperInterface $objectMapper + ) { $this->defaultTimeZone = $userTimezoneService->getDefaultTimezone(); $this->ontology = $ontology; - - // @fixme Inject the mapper - $this->objectMapper = new RdfObjectMapper( - $this->getLogger() - ); + $this->objectMapper = $objectMapper; } public function get(core_kernel_classes_Resource $user): UserSettingsInterface From ef77ec41f018d489394859f76950b533f212a905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Arroyo?= Date: Fri, 11 Mar 2022 16:56:24 +0100 Subject: [PATCH 4/7] refactor: Clean things up a little bit --- models/classes/RdfObjectMapper/Service/RdfObjectMapper.php | 1 - 1 file changed, 1 deletion(-) diff --git a/models/classes/RdfObjectMapper/Service/RdfObjectMapper.php b/models/classes/RdfObjectMapper/Service/RdfObjectMapper.php index 5c4900e95a7..f52022bf2df 100644 --- a/models/classes/RdfObjectMapper/Service/RdfObjectMapper.php +++ b/models/classes/RdfObjectMapper/Service/RdfObjectMapper.php @@ -31,7 +31,6 @@ // right not this is needed (it seems they are not autoloaded for some reason) require_once __DIR__ . '/../Annotation/RdfAttributeMapping.php'; require_once __DIR__ . '/../Annotation/RdfResourceAttributeMapping.php'; -//require_once __DIR__ . '/ResourceHydrator.php'; /** * As Generis explicitly depends on doctrine/annotations ~1.6.0, the current From 6775eb7b5998d4ad89e4ea8b0729c8c47ed64eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Arroyo?= Date: Fri, 11 Mar 2022 17:09:25 +0100 Subject: [PATCH 5/7] chore: Cleanup --- .../UserSettingsMappedType.php | 2 +- models/classes/RdfObjectMapper/NOTES.md | 35 +++++++++++++++++ .../Service/RdfObjectMapper.php | 39 ++----------------- .../implementation/UserSettingsService.php | 2 +- 4 files changed, 41 insertions(+), 37 deletions(-) rename models/classes/RdfObjectMapper/{TargetTypes => Example}/UserSettingsMappedType.php (98%) create mode 100644 models/classes/RdfObjectMapper/NOTES.md diff --git a/models/classes/RdfObjectMapper/TargetTypes/UserSettingsMappedType.php b/models/classes/RdfObjectMapper/Example/UserSettingsMappedType.php similarity index 98% rename from models/classes/RdfObjectMapper/TargetTypes/UserSettingsMappedType.php rename to models/classes/RdfObjectMapper/Example/UserSettingsMappedType.php index ed5c57e63b8..add0f0592c9 100644 --- a/models/classes/RdfObjectMapper/TargetTypes/UserSettingsMappedType.php +++ b/models/classes/RdfObjectMapper/Example/UserSettingsMappedType.php @@ -18,7 +18,7 @@ * Copyright (c) 2021 (original work) Open Assessment Technologies SA. */ -namespace oat\tao\model\RdfObjectMapper\TargetTypes; +namespace oat\tao\model\RdfObjectMapper\Example; use oat\generis\model\GenerisRdf; use oat\tao\model\user\UserSettingsInterface; diff --git a/models/classes/RdfObjectMapper/NOTES.md b/models/classes/RdfObjectMapper/NOTES.md new file mode 100644 index 00000000000..91fb3d1de71 --- /dev/null +++ b/models/classes/RdfObjectMapper/NOTES.md @@ -0,0 +1,35 @@ +**WiP** + +## Notes + +- As Generis explicitly depends on `doctrine/annotations ~1.6.0`, the current + implementation uses Doctrine annotations instead without needing additional + deps. + +- taoDockerize uses PHP 7.2 and Generis is supporting `"php": "^7.1"`: We cannot + use native PHP annotations (yet?). + +- This draft provides an interface for an object mapper, and a single + implementation that uses PHPDoc annotations. + +- Right now only reading RDF data has been considered (i.e. not *writing* new + data or updating existing data). + +## Things to consider + +- Maybe this should be in Generis instead? + +- Another possibility would be to have a "root" object mapper that just + delegates the mapping to a "child" mapper class (a chain of responsibility), + so we can have a mapper based on Doctrine annotations while also having the + possibility to implement a different one using PHP native annotations in the + future (without needing to convert existing classes using Doctrine annotations + to the PHP syntax all at once). + + - For that, we may use an approach similar to Doctrine's: if the object has a + `@Whatever` annotation inside a PHPDoc block, we map using Doctrine + annotations; if it has a native annotation (like + `#[RdfResourceAttributeMapping(RdfResourceAttributeMapping::URI)]`), map + +- May be worth considering how this fits with other architectural patterns + currently under discussion. diff --git a/models/classes/RdfObjectMapper/Service/RdfObjectMapper.php b/models/classes/RdfObjectMapper/Service/RdfObjectMapper.php index f52022bf2df..f3cf7b47566 100644 --- a/models/classes/RdfObjectMapper/Service/RdfObjectMapper.php +++ b/models/classes/RdfObjectMapper/Service/RdfObjectMapper.php @@ -32,32 +32,6 @@ require_once __DIR__ . '/../Annotation/RdfAttributeMapping.php'; require_once __DIR__ . '/../Annotation/RdfResourceAttributeMapping.php'; -/** - * As Generis explicitly depends on doctrine/annotations ~1.6.0, the current - * implementation uses Doctrine annotations instead without needing additional - * deps. - * - * taoDockerize uses PHP 7.2 and Generis is supporting "php": "^7.1": We cannot - * use native PHP annotations (yet?). - * - * @todo Maybe this should be in Generis instead? - * - * @todo We may provide the object mapper as an interface instead, and provide a - * single implementation that uses PHPDoc annotations. - * - * @todo Another possibility would be to have a "root" object mapper that just - * delegates the mapping to a "child" mapper class (a chain of - * responsibility), so we can have a mapper based on Doctrine annotations - * while also having the possibility to implement a different one using - * PHP native annotations in the future (without needing to convert - * existing classes using Doctrine annotations to the PHP syntax all at - * once). - * - * @todo May be worth considering how this fits with other architectural patterns - * currently under discussion. - * - * @todo Right now only reading RDF data has been considered - */ class RdfObjectMapper implements RdfObjectMapperInterface { /** @var ResourceHydrator */ @@ -72,12 +46,15 @@ public function __construct(LoggerInterface $logger, ResourceHydrator $hydrator) $this->hydrator = $hydrator; } + /** + * @throws ReflectionException + */ public function mapResource( core_kernel_classes_Resource $resource, string $targetClass ): object { - $reflector = $this->reflect($targetClass); + $reflector = new ReflectionClass($targetClass); $instance = $reflector->newInstanceWithoutConstructor(); $this->hydrator->hydrateInstance($reflector, $resource, $instance); @@ -87,14 +64,6 @@ public function mapResource( return $instance; } - /** - * @throws ReflectionException - */ - private function reflect(string $targetClass): ReflectionClass - { - return new ReflectionClass($targetClass); - } - private function callConstructorIfPresent( ReflectionClass $reflector, object $instance diff --git a/models/classes/user/implementation/UserSettingsService.php b/models/classes/user/implementation/UserSettingsService.php index 9b994b5208f..16560c6a749 100644 --- a/models/classes/user/implementation/UserSettingsService.php +++ b/models/classes/user/implementation/UserSettingsService.php @@ -28,7 +28,7 @@ use oat\oatbox\user\UserTimezoneServiceInterface; use oat\tao\model\RdfObjectMapper\Contract\RdfObjectMapperInterface; use oat\tao\model\RdfObjectMapper\RdfObjectMapper; -use oat\tao\model\RdfObjectMapper\TargetTypes\UserSettingsMappedType; +use oat\tao\model\RdfObjectMapper\Example\UserSettingsMappedType; use oat\tao\model\user\UserSettingsInterface; use oat\tao\model\user\UserSettingsServiceInterface; use core_kernel_classes_Resource; From d84dba5d239acd79c5d01e9e5a8848266e8ff005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Arroyo?= Date: Fri, 11 Mar 2022 17:13:11 +0100 Subject: [PATCH 6/7] chore: Cleanup --- models/classes/user/implementation/UserSettingsService.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/models/classes/user/implementation/UserSettingsService.php b/models/classes/user/implementation/UserSettingsService.php index 16560c6a749..f78c27aabb8 100644 --- a/models/classes/user/implementation/UserSettingsService.php +++ b/models/classes/user/implementation/UserSettingsService.php @@ -24,7 +24,6 @@ use oat\generis\model\data\Ontology; use oat\generis\model\GenerisRdf; -use oat\oatbox\log\LoggerAwareTrait; use oat\oatbox\user\UserTimezoneServiceInterface; use oat\tao\model\RdfObjectMapper\Contract\RdfObjectMapperInterface; use oat\tao\model\RdfObjectMapper\RdfObjectMapper; @@ -32,12 +31,9 @@ use oat\tao\model\user\UserSettingsInterface; use oat\tao\model\user\UserSettingsServiceInterface; use core_kernel_classes_Resource; -use Psr\Log\LoggerAwareInterface; -class UserSettingsService implements UserSettingsServiceInterface, LoggerAwareInterface +class UserSettingsService implements UserSettingsServiceInterface { - use LoggerAwareTrait; - /** @var Ontology */ private $ontology; From 194dfb0ef51cf2209905d5f001b21e333b73993f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Arroyo?= Date: Fri, 11 Mar 2022 17:45:30 +0100 Subject: [PATCH 7/7] chore: Update notes --- .../Annotation/RdfAttributeMapping.php | 3 ++- models/classes/RdfObjectMapper/NOTES.md | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/models/classes/RdfObjectMapper/Annotation/RdfAttributeMapping.php b/models/classes/RdfObjectMapper/Annotation/RdfAttributeMapping.php index 741448e6471..5f61d13b548 100644 --- a/models/classes/RdfObjectMapper/Annotation/RdfAttributeMapping.php +++ b/models/classes/RdfObjectMapper/Annotation/RdfAttributeMapping.php @@ -20,6 +20,7 @@ namespace oat\tao\model\RdfObjectMapper\Annotation; +use core_kernel_classes_Property; use core_kernel_classes_Resource; use Psr\Log\LoggerInterface; use ReflectionProperty; @@ -72,7 +73,7 @@ public function hydrate( $logger->debug(__CLASS__ . " maps a value to the property"); $values = $src->getPropertyValues( - new \core_kernel_classes_Property($this->propertyUri) + new core_kernel_classes_Property($this->propertyUri) ); if(count($values) == 0) { diff --git a/models/classes/RdfObjectMapper/NOTES.md b/models/classes/RdfObjectMapper/NOTES.md index 91fb3d1de71..606f12e5418 100644 --- a/models/classes/RdfObjectMapper/NOTES.md +++ b/models/classes/RdfObjectMapper/NOTES.md @@ -33,3 +33,15 @@ - May be worth considering how this fits with other architectural patterns currently under discussion. + +- Performance: Right now this always hydrates values for all properties (one by + one) by calling getPropertyValues for each of them. This is done at + RdfAttributeMapping (which may not be the best place to do so) by calling + Resource::getPropertyValues($uri) for each property. + +# TODO + +- Test object inheritance: Having, say, a user class defined somewhere and + another user class extending that one (for example, a class in an extension + that extends that one) should allow having all RDF data for that resource + mapped into the object. \ No newline at end of file