diff --git a/config/services.yaml b/config/services.yaml
index 9689bf0..1f4bb1f 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -30,4 +30,9 @@ services:
class: Atoolo\Resource\Loader\SiteKitResourceHierarchyLoader
arguments:
- '@atoolo_resource.cached_resource_loader'
- - 'category'
\ No newline at end of file
+ - 'category'
+
+ atoolo_resource.resource_capability_factory:
+ class: Atoolo\Resource\ResourceCapabilityFactory
+ arguments:
+ - []
\ No newline at end of file
diff --git a/src/AbstractResource.php b/src/AbstractResource.php
new file mode 100644
index 0000000..2e9c331
--- /dev/null
+++ b/src/AbstractResource.php
@@ -0,0 +1,20 @@
+location, $this->lang);
+ }
+}
diff --git a/src/AtooloResourceBundle.php b/src/AtooloResourceBundle.php
index 2e39ee4..0dafd13 100644
--- a/src/AtooloResourceBundle.php
+++ b/src/AtooloResourceBundle.php
@@ -4,11 +4,8 @@
namespace Atoolo\Resource;
-use Symfony\Component\Config\FileLocator;
-use Symfony\Component\Config\Loader\GlobFileLoader;
-use Symfony\Component\Config\Loader\LoaderResolver;
+use Atoolo\Resource\DependencyInjection\Compiler\ResourceCapabilityValidatorPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
-use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\HttpKernel\Bundle\Bundle;
/**
@@ -18,18 +15,7 @@ class AtooloResourceBundle extends Bundle
{
public function build(ContainerBuilder $container): void
{
- $configDir = __DIR__ . '/../config';
-
- $locator = new FileLocator($configDir);
- $loader = new GlobFileLoader($locator);
- $loader->setResolver(
- new LoaderResolver(
- [
- new YamlFileLoader($container, $locator),
- ],
- ),
- );
-
- $loader->load('services.yaml');
+ parent::build($container);
+ $container->addCompilerPass(new ResourceCapabilityValidatorPass());
}
}
diff --git a/src/Capabilities/MetadataCapability.php b/src/Capabilities/MetadataCapability.php
new file mode 100644
index 0000000..f9d0ace
--- /dev/null
+++ b/src/Capabilities/MetadataCapability.php
@@ -0,0 +1,73 @@
+ tag.
+ * Crucial for SEO and browser tab text.
+ */
+ public function getTitle(): string;
+
+ /**
+ * Returns the content for the tag.
+ * Often used by search engines for the result snippet.
+ */
+ public function getDescription(): string;
+
+ /**
+ * Returns a list of keywords for the tag.
+ * @return string[]
+ */
+ public function getKeywords(): array;
+
+ /**
+ * Returns the name of the resource's author.
+ */
+ public function getAuthor(): ?string;
+
+ /**
+ * Returns the initial publication date.
+ */
+ public function getPublishDate(): ?\DateTimeInterface;
+
+ /**
+ * Returns the date the resource was last modified.
+ */
+ public function getModifiedDate(): ?\DateTimeInterface;
+
+ /**
+ * Returns the canonical URL to prevent duplicate content issues.
+ */
+ public function getCanonicalUrl(): ?string;
+
+ /**
+ * Determines if search engine robots should index this page.
+ */
+ public function isIndexable(): bool;
+
+ /**
+ * Determines if search engine robots should follow links on this page.
+ */
+ public function isFollowable(): bool;
+
+ /**
+ * Returns the title for social media sharing (Open Graph, Twitter Cards).
+ * Falls back to getTitle() if not specified.
+ */
+ public function getSocialShareTitle(): string;
+
+ /**
+ * Returns the description for social media sharing.
+ * Falls back to getDescription() if not specified.
+ */
+ public function getSocialShareDescription(): string;
+
+ /**
+ * Returns the URL for the social media sharing image.
+ */
+ public function getSocialShareImageUrl(): ?string;
+}
diff --git a/src/Capabilities/SiteKitMetadataCapability.php b/src/Capabilities/SiteKitMetadataCapability.php
new file mode 100644
index 0000000..0fa9c02
--- /dev/null
+++ b/src/Capabilities/SiteKitMetadataCapability.php
@@ -0,0 +1,86 @@
+data = $resource->data;
+ }
+
+ public function getTitle(): string
+ {
+ return $this->data->getString('metadata.headline');
+ }
+
+ public function getDescription(): string
+ {
+ return $this->data->getString('metadata.description');
+ }
+
+ public function getKeywords(): array
+ {
+ /** @var string[] $keywords */
+ $keywords = $this->data->getArray('metadata.keywords');
+ return $keywords;
+ }
+
+ public function getAuthor(): ?string
+ {
+ return null; // TODO
+ }
+
+ public function getPublishDate(): ?\DateTimeInterface
+ {
+ return null; // TODO
+ }
+
+
+ public function getModifiedDate(): ?\DateTimeInterface
+ {
+ return null; // TODO
+ }
+
+ public function getCanonicalUrl(): ?string
+ {
+ return null; // TODO
+ }
+
+
+ public function isIndexable(): bool
+ {
+ return true; // TODO
+ }
+
+ public function isFollowable(): bool
+ {
+ return true; // TODO
+ }
+
+ public function getSocialShareTitle(): string
+ {
+ return $this->getTitle(); // TODO
+ }
+
+ public function getSocialShareDescription(): string
+ {
+ return $this->getDescription(); // TODO
+ }
+
+ public function getSocialShareImageUrl(): ?string
+ {
+ return null; // TODO
+ }
+}
diff --git a/src/DependencyInjection/AtooloResourceExtension.php b/src/DependencyInjection/AtooloResourceExtension.php
new file mode 100644
index 0000000..71f95be
--- /dev/null
+++ b/src/DependencyInjection/AtooloResourceExtension.php
@@ -0,0 +1,48 @@
+processConfiguration($configuration, $configs);
+ $configDir = __DIR__ . '/../../config';
+ $locator = new FileLocator($configDir);
+ $loader = new GlobFileLoader($locator);
+ $loader->setResolver(
+ new LoaderResolver(
+ [
+ new YamlFileLoader($container, $locator),
+ ],
+ ),
+ );
+ $loader->load('services.yaml');
+
+ $container->setParameter('atoolo_resource.resources', $config['resources']);
+ $this->initResourceCapabilityFactory($config, $container);
+ }
+
+ // @phpstan-ignore-next-line
+ private function initResourceCapabilityFactory(array $config, ContainerBuilder $container): void
+ {
+ $capabilityImplementationMap = [];
+
+ foreach ($config['resources'] as $resourceClass => $resourceDefinition) {
+ foreach ($resourceDefinition['capabilities'] as $capabilityInterface => $capabilityDefinition) {
+ $capabilityImplementationMap[$resourceClass][$capabilityInterface]
+ = $capabilityDefinition['implementation'];
+ }
+ }
+ $resourceCapabilityFactoryDef = $container->getDefinition('atoolo_resource.resource_capability_factory');
+ $resourceCapabilityFactoryDef->setArgument('$capabilityImplementationMap', $capabilityImplementationMap);
+ }
+}
diff --git a/src/DependencyInjection/Compiler/ResourceCapabilityValidatorPass.php b/src/DependencyInjection/Compiler/ResourceCapabilityValidatorPass.php
new file mode 100644
index 0000000..133ed59
--- /dev/null
+++ b/src/DependencyInjection/Compiler/ResourceCapabilityValidatorPass.php
@@ -0,0 +1,71 @@
+hasParameter('atoolo_resource.resources')) {
+ return;
+ }
+
+ $resourceDefinitions = $container->getParameter('atoolo_resource.resources');
+ // @phpstan-ignore-next-line
+ foreach ($resourceDefinitions as $resourceClass => $resourceDefinition) {
+ foreach ($resourceDefinition['capabilities'] as $capabilityInterface => $capabilityDefinition) {
+
+ if (!interface_exists($capabilityInterface)) {
+ throw new LogicException(sprintf(
+ 'Configuration error: The capability interface "%s" requested for resource "%s" does not exist.',
+ $capabilityInterface,
+ $resourceClass,
+ ));
+ }
+
+ $capabilityImplementation = $capabilityDefinition['implementation'];
+
+ try {
+ $reflectionClass = new \ReflectionClass($capabilityImplementation);
+ $constructor = $reflectionClass->getConstructor();
+
+
+ if ($constructor === null || $constructor->getNumberOfParameters() === 0) {
+ continue; // No constructor or no parameters, nothing to validate.
+ }
+
+ $firstParam = $constructor->getParameters()[0];
+ $requiredType = $firstParam->getType();
+
+ if ($requiredType === null || !$requiredType instanceof \ReflectionNamedType) {
+ continue; // No type-hint, cannot validate.
+ }
+
+ $requiredResourceClass = $requiredType->getName();
+
+ // Check if the configured resource is the same as, or a subclass of, the required one.
+ if (!is_a($resourceClass, $requiredResourceClass, true)) {
+ throw new LogicException(sprintf(
+ 'Configuration error: The capability implementation "%s" requires '
+ . 'a resource of type "%s", but is configured for "%s".',
+ $capabilityImplementation,
+ $requiredResourceClass,
+ $resourceClass,
+ ));
+ }
+ } catch (\ReflectionException $e) {
+ // Could not reflect on the class, might not exist.
+ throw new LogicException(sprintf(
+ 'Configuration error: The capability implementation "%s" requested for resource "%s" does not exist.',
+ $capabilityImplementation,
+ $resourceClass,
+ ));
+ }
+ }
+ }
+ }
+}
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
new file mode 100644
index 0000000..d64292f
--- /dev/null
+++ b/src/DependencyInjection/Configuration.php
@@ -0,0 +1,36 @@
+getRootNode()
+ ->children()
+ ->arrayNode('resources')
+ ->useAttributeAsKey('name')
+ ->arrayPrototype()
+ ->children()
+ ->arrayNode('capabilities')
+ ->useAttributeAsKey('interface')
+ ->arrayPrototype()
+ ->children()
+ ->scalarNode('implementation')->isRequired()->end()
+ ->end()
+ ->end()
+ ->end()
+ ->end()
+ ->end()
+ ->end()
+ ->end();
+
+ return $treeBuilder;
+ }
+}
diff --git a/src/Resource.php b/src/Resource.php
index 389dce7..9dbd9a8 100644
--- a/src/Resource.php
+++ b/src/Resource.php
@@ -5,22 +5,19 @@
namespace Atoolo\Resource;
/**
- * In the Atoolo context, resources are aggregated data from
+ * In the Atoolo context, sitekit resources are aggregated data from
* IES (Sitepark's content management system).
*/
-class Resource
+class Resource extends AbstractResource
{
public function __construct(
- public readonly string $location,
- public readonly string $id,
- public readonly string $name,
+ string $location,
+ string $id,
+ string $name,
public readonly string $objectType,
- public readonly ResourceLanguage $lang,
+ ResourceLanguage $lang,
public readonly DataBag $data,
- ) {}
-
- public function toLocation(): ResourceLocation
- {
- return ResourceLocation::of($this->location, $this->lang);
+ ) {
+ parent::__construct($location, $id, $name, $lang);
}
}
diff --git a/src/ResourceCapabilityFactory.php b/src/ResourceCapabilityFactory.php
new file mode 100644
index 0000000..6c52a60
--- /dev/null
+++ b/src/ResourceCapabilityFactory.php
@@ -0,0 +1,47 @@
+> $capabilityImplementationMap
+ */
+ public function __construct(
+ private readonly array $capabilityImplementationMap,
+ ) {}
+
+ /**
+ * @template T
+ * @param class-string $capabilityInterface
+ * @return T|null
+ */
+ public function create(AbstractResource $resource, string $capabilityInterface): mixed
+ {
+ $resourceClass = get_class($resource);
+
+ if (!isset($this->capabilityImplementationMap[$resourceClass])) {
+ $this->logger?->warning(
+ 'no resource config found for resource class',
+ [
+ 'resource_class' => $resourceClass,
+ ],
+ );
+ return null;
+ }
+ $capabilityImplementation = $this->capabilityImplementationMap[$resourceClass][$capabilityInterface] ?? null;
+ if ($capabilityImplementation === null) {
+ return null;
+ }
+ /** @var T $capabilityInstance */
+ $capabilityInstance = new $capabilityImplementation($resource);
+ return $capabilityInstance;
+ }
+}
diff --git a/src/Service/ResourceCapabilityFactoryExampleService.php b/src/Service/ResourceCapabilityFactoryExampleService.php
new file mode 100644
index 0000000..9931cd1
--- /dev/null
+++ b/src/Service/ResourceCapabilityFactoryExampleService.php
@@ -0,0 +1,32 @@
+resourceCapabilityFactory->create(
+ $resource,
+ MetadataCapability::class,
+ );
+
+ if ($metadata === null) {
+ echo "resource has no metadata";
+ return;
+ }
+
+ echo $metadata->getTitle();
+ }
+}