Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,9 @@ services:
class: Atoolo\Resource\Loader\SiteKitResourceHierarchyLoader
arguments:
- '@atoolo_resource.cached_resource_loader'
- 'category'
- 'category'

atoolo_resource.resource_capability_factory:
class: Atoolo\Resource\ResourceCapabilityFactory
arguments:
- []
20 changes: 20 additions & 0 deletions src/AbstractResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Atoolo\Resource;

abstract class AbstractResource
{
public function __construct(
public readonly string $location,
public readonly string $id,
public readonly string $name,
public readonly ResourceLanguage $lang,
) {}

public function toLocation(): ResourceLocation
{
return ResourceLocation::of($this->location, $this->lang);
}
}
20 changes: 3 additions & 17 deletions src/AtooloResourceBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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());
}
}
73 changes: 73 additions & 0 deletions src/Capabilities/MetadataCapability.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

declare(strict_types=1);

namespace Atoolo\Resource\Capabilities;

interface MetadataCapability
{
/**
* Returns the content for the HTML <title> tag.
* Crucial for SEO and browser tab text.
*/
public function getTitle(): string;

/**
* Returns the content for the <meta name="description"> tag.
* Often used by search engines for the result snippet.
*/
public function getDescription(): string;

/**
* Returns a list of keywords for the <meta name="keywords"> 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;
}
86 changes: 86 additions & 0 deletions src/Capabilities/SiteKitMetadataCapability.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

declare(strict_types=1);

namespace Atoolo\Resource\Capabilities;

use Atoolo\Resource\AbstractResource;
use Atoolo\Resource\DataBag;
use Atoolo\Resource\Resource;

class SiteKitMetadataCapability implements MetadataCapability
{
private DataBag $data;

public function __construct(AbstractResource $resource)
{
if (!($resource instanceof Resource)) {
throw new \RuntimeException('resource is not of type ' . Resource::class);
}
$this->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
}
}
48 changes: 48 additions & 0 deletions src/DependencyInjection/AtooloResourceExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace Atoolo\Resource\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Loader\GlobFileLoader;
use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

class AtooloResourceExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = new Configuration();
$config = $this->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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

namespace Atoolo\Resource\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\LogicException;

class ResourceCapabilityValidatorPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->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,
));
}
}
}
}
}
36 changes: 36 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Atoolo\Resource\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('atoolo_resource');

// @phpstan-ignore-next-line
$treeBuilder->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;
}
}
Loading