Changing and Extending

If you need additional functionality or the existing 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 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'].

Return type

array

setEntryPointParts($entryPointParts)
Parameters
  • $entryPointParts (array) --

getRequest()
Return type

PsrHttpMessageServerRequestInterface

setRequest($request)
Parameters
  • $request (PsrHttpMessageServerRequestInterface) --

class Pixelant\Interest\DataHandling\Operation\Event\RecordOperationSetupEvent

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\RecordOperationEventHandlerInterface.

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()
Return type

PixelantInterestDataHandlingOperationAbstractRecordOperation

class Pixelant\Interest\DataHandling\Operation\Event\RecordOperationInvocationEvent

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()
Return type

PixelantInterestDataHandlingOperationAbstractRecordOperation

class 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()
Return type

PsrHttpMessageResponseInterface

setResponse($response)
Parameters
  • $response (PsrHttpMessageResponseInterface) --

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'
    )
);

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 share its API. CreateRecordOperation and CreateRecordOperation are direct subclasses of AbstractConstructiveRecordOperation, which adds a more complex constructor.

class Pixelant\Interest\DataHandling\Operation\AbstractConstructiveRecordOperation
class Pixelant\Interest\DataHandling\Operation\CreateRecordOperation
class Pixelant\Interest\DataHandling\Operation\UpdateRecordOperation
__construct($recordRepresentation, $metaData)
Parameters
  • $recordRepresentation (PixelantInterestDomainModelDtoRecordRepresentation) --

  • $metaData (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(),
];
Return type

TYPO3CMSFrontendContentObjectContentObjectRenderer

dispatchMessage($message)
Parameters
  • $message (PixelantInterestDataHandlingOperationMessageMessageInterface) --

Dispatch a message, to be picked up later, in another part of the operation's execution flow.

Return type

mixed

getDataFieldForDataHandler($fieldName)
Parameters
  • $fieldName (string) --

Get the value of a specific field in the data for DataHandler. Same as $this->getDataForDataHandler()[$fieldName].

Return type

mixed

getDataForDataHandler()

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

Return type

array

getDataHandler()

Returns the internal DataHandler object used in the operation.

Return type

PixelantInterestDataHandlingDataHandler

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.

Return type

string

getLanguage()

Returns the record language represented by a \TYPO3\CMS\Core\Site\Entity\SiteLanguage object, if set. $this->getRecordRepresentation()->getRecordInstanceIdentifier()->getLanguage()

Return type

TYPO3CMSCoreSiteEntitySiteLanguage|null

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

Return type

array

getRemoteId()

Returns the table name. Shortcut for $this->getRecordRepresentation()->getRecordInstanceIdentifier()->getRemoteIdWithAspects()

Return type

string

getTable()

Returns the table name. Shortcut for $this->getRecordRepresentation()->getRecordInstanceIdentifier()->getTable()

Return type

string

getRecordRepresentation()
Return type

PixelantInterestDomainModelDtoRecordRepresentation

getStoragePid()

Gets the PID of the record as originally set during object construction, usually by the \Pixelant\Interest\DataHandling\Operation\Event\Handler\ResolveStoragePid event.

Return type

void

getSettings()

Returns the settings array from UserTS (tx_interest.*).

Return type

array

getUid()

Returns the record UID, or zero if not yet set. $this->getRecordRepresentation()->getRecordInstanceIdentifier()->getUid()

Return type

int

getUidPlaceholder()

Returns a DataHandler UID placeholder. If it has not yet been set, it will be generated as a random string prefixed with "NEW". $this->getRecordRepresentation()->getRecordInstanceIdentifier()->getUidPlaceholder()

Return type

string

hasExecuted()

Returns true if the operation has executed the DataHandler operations.

Return type

bool

isDataFieldSet($fieldName)
Parameters
  • $fieldName (string) --

Check if a field in the data array is set. Same as isset($this->getDataForDataHandler()[$fieldName]).

Return type

bool

isSuccessful()

Returns true if the operation has executed the DataHandler operations without errors.

Return type

bool

retrieveMessage($message)
Parameters
  • $messageFqcn (string) --

Pick the last message of class $messageFqcn from the message queue. Returns null if no messages are left in the queue.

Return type

PixelantInterestDataHandlingOperationMessageMessageInterface|null

setDataFieldForDataHandler($fieldName, $value)
Parameters
  • $fieldName (string) --

Set the value of a specific field in the data for DataHandler. Same as:

Return type

void

setDataForDataHandler($dataForDataHandler)

Set the data that will be written to the DataHandler.

Parameters
  • $dataForDataHandler (array) --

setHash($hash)
Parameters
  • $hash (string) --

Override the record operation's uniqueness hash. Changing this value can have severe consequences for data integrity.

Return type

void

setStoragePid($storagePid)
Parameters
  • $storagePid (int) --

Sets the storage PID. This might override a PID set by the \Pixelant\Interest\DataHandling\Operation\Event\Handler\ResolveStoragePid event, which usually handles this task.

Return type

void

setUid($uid)
Parameters
  • $uid (int) --

Sets the record UID. $this->getRecordRepresentation()->getRecordInstanceIdentifier()->setUid($uid)

Return type

void

unsetDataField($fieldName)
Parameters
  • $fieldName (string) --

Unset a field in the data array. Same as:

Return type

void

class Pixelant\Interest\DataHandling\Operation\DeleteRecordOperation
__construct($recordRepresentation)

You cannot send metadata information to a delete operation.

Parameters
  • $recordRepresentation (PixelantInterestDomainModelDtoRecordRepresentation) --

Record Operation Messages

Classes implementing \Pixelant\Interest\DataHandling\Operation\Message\MessageInterface can be used to carry information within the execution flow of an instance of \Pixelant\Interest\DataHandling\Operation\AbstractRecordOperation. This is especially useful between EventHandlers.

For example, \Pixelant\Interest\DataHandling\Operation\Event\Handler\Message\PendingRelationMessage is used to carry information about pending relations between the event that discovers them and the event that persists the information to the database — if the record operation was successful.

Sending a message in \Pixelant\Interest\DataHandling\Operation\Event\Handler\MapUidsAndExtractPendingRelations:

Retrieving messages and using the message data to persist the information to the database in \Pixelant\Interest\DataHandling\Operation\Event\Handler\PersistPendingRelationInformation:

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.

Warning

You should never access the tx_interest_remote_id_mapping table directly, but use the classes and methods described here.

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 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.

Parameters
  • $remoteId (string) -- The remote ID of the record to touch.

touched($remoteId)

Returns the touched timestamp for the record.

Parameters
  • $remoteId (string) -- The remote ID of the record to touch.

Return type

int

findAllUntouchedSince($timestamp, $excludeManual = true)

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

Parameters
  • $timestamp (int) -- Unix timestamp.

  • $excludeManual (bool) -- 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.

Return type

bool

findAllTouchedSince($timestamp, $excludeManual = true)

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

Parameters
  • $timestamp (int) -- Unix timestamp.

  • $excludeManual (bool) -- 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.

Return type

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)
        );
    ))();
}

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

Warning

Make sure that you don't mix up the metadata in the mapping table with the metadata that is sent to operations, e.g. using the metaData property or the --metaData option. These are not related.

Note

The field data is encoded as JSON. Any objects must be serialized so they can be stored as a string.

Relevant methods
class Pixelant\Interest\Domain\Repository\RemoteIdMappingRepository
getMetaData($remoteId)

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

Parameters
  • $remoteId (string) -- The remote ID of the record to return the metadata for.

Return type

array

getMetaDataValue($remoteId, $key)

Retrieves a metadata entry.

Parameters
  • $remoteId (string) -- The remote ID of the record to return the metadata for.

  • $key (string) -- The originator class's fully qualified class name.

Return type

string, float, int, array, or null

getMetaDataValue($remoteId, $key, $value)

Sets a metadata entry.

Parameters
  • $remoteId (string) -- The remote ID of the record to return the metadata for.

  • $key (string) -- The originator class's fully qualified class name.

  • $value (string|float|int|array|null) -- 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();