Changing and Extending

If you need additional functionality or the extisting functionality of the extension isn't quite what you need, this section tells you how to change the behavior of the Interest extension. It also tells you how to extend the functionality, as well as a bit about the extension's inner workings.

PSR-14 Events

Events

The events are listed in order of execution.

class HttpRequestRouterHandleByEvent
Fully qualified name
\Pixelant\Interest\Router\Event\HttpRequestRouterHandleByEvent

Called in HttpRequestRouter::handleByMethod(). Can be used to modify the request and entry point parts before they are passed on to a RequestHandler.

EventHandlers for this event should implement \Pixelant\Interest\Router\Event\HttpRequestRouterHandleByEventHandlerInterface.

getEntryPointParts ( )

Returns an array of the entry point parts, i.e. the parts of the URL used to detect the correct entry point. Given the URL http://www.example.com/rest/tt_content/ContentRemoteId and the default entry point rest, the entry point parts will be ['tt_content', 'ContentRemoteId'].

returntype

array

setEntryPointParts ( $entryPointParts)
param array $entryPointParts
 
getRequest ( )
returntype

PsrHttpMessageServerRequestInterface

setRequest ( $request)
param Psr\Http\Message\ServerRequestInterface $request
 
class BeforeRecordOperationEvent
Fully qualified name
\Pixelant\Interest\DataHandling\Operation\Event\BeforeRecordOperationEvent

Called inside the AbstractRecordOperation::__construct() when a *RecordOperation object has been initialized, but before data validations.

EventHandlers for this event should implement \Pixelant\Interest\DataHandling\Operation\Event\BeforeRecordOperationEventHandlerInterface.

EventHandlers for this event can throw these exceptions:

\Pixelant\Interest\DataHandling\Operation\Event\Exception\StopRecordOperationException
To quietly stop the record operation. This exception is only logged as informational and the operation will be treated as successful. E.g. used when deferring an operation.
\Pixelant\Interest\DataHandling\Operation\Event\Exception\BeforeRecordOperationEventException
Will stop the record operation and log as an error. The operation will be treated as unsuccessful.
getRecordOperation ( )
returntype

PixelantInterestDataHandlingOperationAbstractRecordOperation

class AfterRecordOperationEvent
Fully qualified name
\Pixelant\Interest\DataHandling\Operation\Event\AfterRecordOperationEvent

Called as the last thing inside the AbstractRecordOperation::__invoke() method, after all data persistence and pending relations have been resolved.

EventHandlers for this event should implement \Pixelant\Interest\DataHandling\Operation\Event\AfterRecordOperationEventHandlerInterface.

getRecordOperation ( )
returntype

PixelantInterestDataHandlingOperationAbstractRecordOperation

class HttpResponseEvent
Fully qualified name
\Pixelant\Interest\Middleware\Event\HttpResponseEvent

Called in the middleware, just before control is handled back over to TYPO3 during an HTTP request. Allows modification of the response object.

EventHandlers for this event should implement \Pixelant\Interest\Middleware\Event\HttpResponseEventHandlerInterface.

getResponse ( )
returntype

PsrHttpMessageResponseInterface

setResponse ( $response)
param Psr\Http\Message\ResponseInterface $response
 

In TYPO3 version 9

TYPO3 version 9 doesn't support PSR-14 events, but it's using signals and slots instead. Luckily, PSR-14 Events and EventHandlers can be made to work with them as well. You can register an EventHandler as a SignalSlot using this convenience function:

\Pixelant\Interest\Utility\CompatibilityUtility::registerEventHandlerAsSignalSlot(
    string $eventClassName,
    string $eventHandlerClassName
);
Copied!

It will map the class and methods correctly. The only difference is that you can't change the order of execution, as the slots are called in the order they are registered.

Here's how the function is used by the Interest extension itself:

\Pixelant\Interest\Utility\CompatibilityUtility::registerEventHandlerAsSignalSlot(
    \Pixelant\Interest\DataHandling\Operation\Event\BeforeRecordOperationEvent::class,
    \Pixelant\Interest\DataHandling\Operation\Event\Handler\StopIfRepeatingPreviousRecordOperation::class
);
Copied!

How it works

Internal representation and identity

Inside the extension, a record's state and identity is maintained by two data transfer object classes:

  • A record's unique identity from creation to deletion is represented by \Pixelant\Interest\Domain\Model\Dto\RecordInstanceIdentifier.
  • A record's current state, including the data that should be written to the database is represented by \Pixelant\Interest\Domain\Model\Dto\RecordRepresentation.

When creating a RecordRepresentation, you must also supply a RecordInstanceIdentifier:

use Pixelant\Interest\Domain\Model\Dto\RecordInstanceIdentifier;
use Pixelant\Interest\Domain\Model\Dto\RecordRepresentation;

new RecordRepresentation(
    [
        'title' => 'My record title',
        'bodytext' => 'This is a story about ...',
    ],
    new RecordInstanceIdentifier(
        'tt_content',
        'ContentElementA',
        'en'
    )
);
Copied!

Record operations

Record operations are the core of the Interest extension. Each represents one operation requested from the outside. One record operation is not the same as one database operation. Some record operations will not be executed (if it is a duplicate of the previous operation on the same remote ID) or deferred (if the record operation requires a condition to be fulfilled before it can be executed).

The record operations are invokable, and are executed as such:

Record operation types

There are three record operations:

  • Create
  • Update
  • Delete

All are subclasses of \Pixelant\Interest\DataHandling\Operation\AbstractRecordOperation, and both Create and Update share its API, while Delete has a reduced constructor.

.. info::

The constructor of \Pixelant\Interest\DataHandling\Operation\DeleteRecordOperation might change in the future. A delete operation requires no field data. It is unnecessary (and only a requirement of the parent class) to require a \Pixelant\Interest\Domain\Model\Dto\RecordRepresentation. It should be sufficient to supply a \Pixelant\Interest\Domain\Model\Dto\RecordInstanceIdentifier.

class AbstractRecordOperation
Fully qualified name
\Pixelant\Interest\DataHandling\Operation\AbstractRecordOperation
.. php:currentnamespace:: Pixelant\Interest\DataHandling\Operation
class CreateRecordOperation
Fully qualified name
\Pixelant\Interest\DataHandling\Operation\CreateRecordOperation
.. php:currentnamespace:: Pixelant\Interest\DataHandling\Operation
class UpdateRecordOperation
Fully qualified name
\Pixelant\Interest\DataHandling\Operation\UpdateRecordOperation
__construct ( $recordRepresentation, $metaData)
param Pixelant\Interest\Domain\Model\Dto\RecordRepresentation $recordRepresentation
 
param array $metaData
 
getDataForDataHandler ( )

Get the data that will be written to the DataHandler. This is a modified version of the data in $this->getRecordRepresentation()->getData().

returntype

array

setDataForDataHandler ( $dataForDataHandler)

Set the data that will be written to the DataHandler.

param array $dataForDataHandler
 
getRecordRepresentation ( )
returntype

PixelantInterestDomainModelDtoRecordRepresentation

getMetaData ( )

Returns the metadata array for the operation. This metadata is not used other than to generate the uniqueness hash for the operation. You can use it to transfer useful information, e.g. for transformations. See: Accessing metadata

returntype

array

getContentRenderer ( )

Returns a special ContentObjectRenderer for this operation. The data array is populated with operation-specific information when the operation object is initialized. It is not updated if this information changes.

 $contentObjectRenderer->data = [
    'table' => $this->getTable(),
    'remoteId' => $this->getRemoteId(),
    'language' => $this->getLanguage()->getHreflang(),
    'workspace' => null,
    'metaData' => $this->getMetaData(),
    'data' => $this->getDataForDataHandler(),
];
Copied!
returntype

TYPO3CMSFrontendContentObjectContentObjectRenderer

getHash ( )

Get the unique hash of this operation. The hash is generated when the operation object is initialized, and it is not changed. This hash makes it possible for the Interest extension to know whether the same operation has been run before.

returntype

string

.. php:currentnamespace:: Pixelant\Interest\DataHandling\Operation
class DeleteRecordOperation
Fully qualified name
\Pixelant\Interest\DataHandling\Operation\DeleteRecordOperation
__construct ( $recordRepresentation)

You cannot send metadata information to a delete operation.

param Pixelant\Interest\Domain\Model\Dto\RecordRepresentation $recordRepresentation
 

Mapping table

The extension keeps track of the mapping between remote IDs and TYPO3 records in the table tx_interest_remote_id_mapping. In addition to mapping information, the table contains metadata about each record.

Touching and the touched

When a record is created or updated, the touched timestamp is updated. The timestamp is also updated if the remote request intended to update the record, but the Interest extension decided not to do it, for example because there was nothing to change. In this way, the time a record was last touched may more recent than the record's modification date.

The time the record was last touched can help you verify that a request was processed — or to find the remote IDs that were not mentioned at all. In the latter case, knowing remote IDs that are no longer updated regularly can tell you which remote IDs should be deleted.

Relevant methods
class RemoteIdMappingRepository
Fully qualified name
\Pixelant\Interest\Domain\Repository\RemoteIdMappingRepository
touch ( $remoteId)

Touches the remote ID and nothing else. Sets the touched timestamp for the remote ID to the current time.

param string $remoteId

The remote ID of the record to touch.

touched ( $remoteId)

Returns the touched timestamp for the record.

param string $remoteId

The remote ID of the record to touch.

returntype

int

findAllUntouchedSince ( $timestamp, $excludeManual = true)

Returns an array containing all remote IDs that have not been touched since $timestamp.

param int $timestamp

Unix timestamp.

param bool $excludeManual

When true, remote IDs flagged as manual will be excluded from the result. Usually a good idea, as manual entries aren't usually a part of any update workflow.

returntype

bool

findAllTouchedSince ( $timestamp, $excludeManual = true)

Returns an array containing all remote IDs that have been touched since $timestamp.

param int $timestamp

Unix timestamp.

param bool $excludeManual

When true, remote IDs flagged as manual will be excluded from the result. Usually a good idea, as manual entries aren't usually a part of any update workflow.

returntype

bool

Example

Fetching all remote IDs that have not been touched since the same time yesterday.

use Pixelant\Interest\Domain\Model\Dto\RecordInstanceIdentifier;
use Pixelant\Interest\Domain\Model\Dto\RecordRepresentation;
use Pixelant\Interest\Domain\Repository\RemoteIdMappingRepository;
use Pixelant\Interest\DataHandling\Operation\DeleteRecordOperation;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$mappingRepository = GeneralUtility::makeInstance(RemoteIdMappingRepository::class);

foreach($mappingRepository->findAllUntouchedSince(time() - 86400) as $remoteId) {
    (new DeleteRecordOperation(
        new RecordRepresentation(
            [],
            new RecordInstanceIdentifier('table', $remoteId)
        );
    ))();
}
Copied!

Metadata

The mapping table also contains a field that can contain serialized meta information about the record. Any class can add and retrieve meta information from this field.

Here's two existing use cases:

  • Foreign relation sorting order by \Pixelant\Interest\DataHandling\Operation\Event\Handler\ForeignRelationSortingEventHandler
  • File modification info by \Pixelant\Interest\DataHandling\Operation\Event\Handler\PersistFileDataEventHandler
Relevant methods
class RemoteIdMappingRepository
Fully qualified name
\Pixelant\Interest\Domain\Repository\RemoteIdMappingRepository
getMetaData ( $remoteId)

Retrieves all of the metadata entries as a key-value array.

param string $remoteId

The remote ID of the record to return the metadata for.

returntype

array

getMetaDataValue ( $remoteId, $key)

Retrieves a metadata entry.

param string $remoteId

The remote ID of the record to return the metadata for.

param string $key

The originator class's fully qualified class name.

returntype

string, float, int, array, or null

getMetaDataValue ( $remoteId, $key, $value)

Sets a metadata entry.

param string $remoteId

The remote ID of the record to return the metadata for.

param string $key

The originator class's fully qualified class name.

param string|float|int|array|null $value

The value to set.

Example

This simplified excerpt from PersistFileDataEventHandler shows how metadata stored in the record is used to avoid downloading a file if it hasn't changed. If it has changed, new metadata is set.

use GuzzleHttp\Client;
use Pixelant\Interest\Domain\Repository\RemoteIdMappingRepository;

$mappingRepository = GeneralUtility::makeInstance(RemoteIdMappingRepository::class);

$metaData = $mappingRepository->getMetaDataValue(
    $remoteId,
    self::class
) ?? [];

$headers = [
    'If-Modified-Since' => $metaData['date'],
    'If-None-Match'] => $metaData['etag'],
];

$response = GeneralUtility::makeInstance(Client::class)
    ->get($url, ['headers' => $headers]);

if ($response->getStatusCode() === 304) {
    return null;
}

$mappingRepository->setMetaDataValue(
    $remoteId,
    self::class,
    [
        'date' => $response->getHeader('Date'),
        'etag' => $response->getHeader('ETag'),
    ]
);

return $response->getBody()->getContents();
Copied!