Skip to content

feat: introducing resource capabilities (WIP)#43

Closed
sitepark-becker wants to merge 1 commit intomainfrom
feature/resource-capabilities
Closed

feat: introducing resource capabilities (WIP)#43
sitepark-becker wants to merge 1 commit intomainfrom
feature/resource-capabilities

Conversation

@sitepark-becker
Copy link
Contributor

@sitepark-becker sitepark-becker commented Aug 19, 2025

Summary

The goal of this PR is to create a more general and flexible interface for accessing the data of a Resource, to allow for a decoupling of atoolo-bundles from the data structure of Sitekit/Infosite resources.

This is done by introducing an configurable capability/provider model (strategy pattern). Capabilites are interfaces that define which data a resource is "capable" of exposing while the provider is the implementation of such interface.
See section "Introducing a capability/provider model" for a more in-depth explanation.

Problem

Currently, a Atoolo\Resource\Resource is pretty much just a SiteKit resource. It has an id,, a location, a name, a lang, an objectType and some data which is stored as a readonly associative array (DataBag).

Since Resource objects are assumed to be SiteKit resources, we explicitly expect the DataBag to have a certain data structure. There is no abstract interface through which we access the data, so we make direct calls to the data property.
This leads to a tight coupling between atoolo-bundles and the SiteKit specific data structure. E.g. in the atoolo-graphql-search-bundle we call $resource->data->getString('base.teaser.text') for accessing the teaser text of a resource.

Approach

Decoupling from SiteKit by introducing an AbstractResource

A Resource has two properties that I think are rather SiteKit specific:

  • string $objectType is simply a SiteKit specific concept
  • DataBag $data can theoretically be used by all types of resources to store their data, but I think we shouldn't force that. Some resource might rather use an actual, fully typed model. Currently, it seems, that the DataBag is more of a "temporary" solution for SiteKit resources anyway.

For these reasons, I created an AbstractResource, that leaves out the objectType and the data property:

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

The old Resource class then extends this AbstractResource.

class Resource extends AbstractResource
{
    public function __construct(
        string $location,
        string $id,
        string $name,
        public readonly string $objectType,
        ResourceLanguage $lang,
        public readonly DataBag $data,
    ) {
        parent::__construct($location, $id, $name, $lang);
    }
}

Optimally, I think we should rename Resource to SiteKitResource and AbstractResource to Resource, but that would be a breaking change, of course. So keep in mind that for now, Resource specifically refers to a SiteKit resource.

Introducing a capability/provider model

A "Capability" is an interface that defines which data a resource should be capable of exposing for a certain purpose. E.g. a (very simplified!) MetadataCapability could look like this:

interface MetadataCapability
{
    public function getTitle(): string;
    public function getDescription(): string;
}

A "Provider" is an implementation of that capability. It usually takes a resource (subclass of an AbstractResource) as a constructor property and then implements a capability interface by accessing the resources data.
E.g. an implementation of the capability above for a SiteKit resource cold look like this:

class SiteKitMetadataCapability implements MetadataCapability
{
    public function __construct(
        private Resource $resource
    ){}

    public function getTitle(): string
    {
        return $this->data->getString('metadata.headline');
    }

    public function getDescription(): string
    {
        return $this->data->getString('metadata.description');
    }
}

Let's say that we want to access a certain capability of some AbstractResource. This is done through a ResourceCapabilityFactory. The ResourceCapabilityFactory is a service that knows about the capabilities/providers of all resource types.
For example: To get the MetadataCapability of some random AbstractResource $resource, we could do something like this:

class ExampleService
{
    public function __construct(
        private readonly ResourceCapabilityFactory $resourceCapabilityFactory, // access factory through DI
    ) {}

    public function printMetaTitleOfAnyResource(
        AbstractResource $resource,
    ): void {
        $metadata = $this->resourceCapabilityFactory->create(
            $resource,
            MetadataCapability::class,
        );
        // $metadata is now of type MetadataCapability::class or null
        if ($metadata === null) {
            echo "resource has no metadata capability";
            return;
        }
        echo $metadata->getTitle();
    }
}

This way, we successfully accessed the metadata of a resource without knowing about the underlying data structure.

Configuring a resource type

To define which type of resource has which capability, I propose to create a custom symfony config that could look like this:

# config/packages/atoolo_resource.yaml
atoolo_resource:
  resource_definitions:

    Atoolo\Resource\Resource: # disciriminate the resource types by their class names (?)
      capabilities:
        Atoolo\Resource\Capabilities\MetadataCapability: # the key is the capability (interface)
          implementation: Atoolo\Resource\Capabilities\SiteKitMetadataCapability # heres the provider/implementation
        Atoolo\Resource\Capabilities\TeaserCapability: 
          implementation: Atoolo\Resource\Capabilities\SiteKitTeaserCapability
        Atoolo\EventsCalendar\Capabilities\SchedulingCapability: # add capabilities from other atoolo-bundles
          implementation: Atoolo\EventsCalendar\Capabilities\SiteKitSchedulingCapability

    Some\Other\ExternalResource:
      capabilities:
        Atoolo\Resource\Capabilities\MetadataCapability:
           implementation: Some\Other\ExternalResourceMetadataCapability
           # more potential keys
           #options: ...
           #factory: ...

This config (or at least the resource_definition part) is then passed to the ResourceCapabilityFactory during the symfony compilation phase (see class AtooloResourceExtension).

I also added a ResourceCapabilityValidatorPass that reads the config and checks whether all capability providers are compatible with the resource they're defined in. (For example, the SiteKitMetadataCapability can only handle sitekit resource (Atoolo\Resource\Resource) and should therefore only be allowed there).

This config is just a prototype btw. Feel free to add suggestions.

Conculsion

Pros

  • Decoupling: consumer bundles (like atoolo-events-calendar) don't have to know about the inner data structure of a resource. Instead, they can just define a capability interface (and maybe a default sitekit implementation).
  • Type safety: Using distinct resource classes and well defined capabilities automatically provides type safety and a better IDE integration
  • Flexibility/Extensibility: Being able to change and add capabilities at will
  • Testability: Easier unit tests as we only have to test against certain capabilities instead of specific resource data structures

Cons

  • More boilerplate
  • Higher Complexity

Todos, Next Steps, Ideas

  • Rename Atoolo\Resource\Resource to Atoolo\Resource\SiteKitResource
  • Rename Atoolo\Resource\AbstractResource to Atoolo\Resource\Resource
  • Define basic capabilities that are expected to be used in multiple contexts.
    • MetaDataCapability for metadata
    • BaiscContentCapability for headline, intro etc. ??
    • TeaserDataCapability for teaser data
  • For each of those basic capabilities, provide a sitekit provider/implementation
  • migrate each atoolo-bundle that uses resources to the capability/provider model
  • Maybe create a new bunde atoolo-sitekit-bundle that contains all the sitekit specific implementations? Maybe move all SiteKit classes like Resource, SiteKitResourceHierarchyLoader etc. there?

@sitepark-becker sitepark-becker self-assigned this Aug 19, 2025
@sitepark-becker
Copy link
Contributor Author

Closing this PR because of a new approach, see #45

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant