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 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 RecordOperationSetupEvent
Fully qualified name
\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 ( )
returntype

PixelantInterestDataHandlingOperationAbstractRecordOperation

class RecordOperationInvocationEvent
Fully qualified name
\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 ( )
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
 

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

class AbstractConstructiveRecordOperation
Fully qualified name
\Pixelant\Interest\DataHandling\Operation\AbstractConstructiveRecordOperation
.. 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
 
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

dispatchMessage ( $message)
param \Pixelant\Interest\DataHandling\Operation\Message\MessageInterface $message
 

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

returntype

mixed

getDataFieldForDataHandler ( $fieldName)
param string $fieldName
 

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

returntype

mixed

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

getDataHandler ( )

Returns the internal DataHandler object used in the operation.

returntype

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.

returntype

string

getLanguage ( )

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

returntype

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

returntype

array

getRemoteId ( )

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

returntype

string

getTable ( )

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

returntype

string

getRecordRepresentation ( )
returntype

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.

returntype

void

getSettings ( )

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

returntype

array

getUid ( )

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

returntype

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

returntype

string

hasExecuted ( )

Returns true if the operation has executed the DataHandler operations.

returntype

bool

isDataFieldSet ( $fieldName)
param string $fieldName
 

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

returntype

bool

isSuccessful ( )

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

returntype

bool

retrieveMessage ( $message)
param string $messageFqcn
 

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

returntype

PixelantInterestDataHandlingOperationMessageMessageInterface|null

setDataFieldForDataHandler ( $fieldName, $value)
param string $fieldName
 

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

returntype

void

setDataForDataHandler ( $dataForDataHandler)

Set the data that will be written to the DataHandler.

param array $dataForDataHandler
 
setHash ( $hash)
param string $hash
 

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

returntype

void

setStoragePid ( $storagePid)
param int $storagePid
 

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.

returntype

void

setUid ( $uid)
param int $uid
 

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

returntype

void

unsetDataField ( $fieldName)
param string $fieldName
 

Unset a field in the data array. Same as:

returntype

void

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

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.

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!