How to create custom gateways
Table gateway implementations in the package are designed to be usable as-is, fragments that are passed to gateway methods should be generated by either generic or custom builders.
That being said, there are some valid reasons for creating custom implementations of TableGateway:
Table access should be read-only (or append-only);
Gateway needs custom base AST and / or pre-applying some
Fragments;Gateway will access a relation that is not an ordinary table (view, foreign table, …).
The first case is trivial (just make DML methods throw exceptions) and the third one is way out of scope for a simple how-to: it requires at least a custom implementation of metadata interfaces. Thus we’ll cover the remaining case.
Example: soft deletes
We’ll implement a gateway that marks table rows as deleted rather than permanently deletes them. We will also make sure that “deleted” rows are not accessed.
Similar to builders several steps should be performed
Create a new implementation of
TableGateway;Make
TableLocatoraware of the new class;Make IDE and psalm aware of the new class. Gateways aren’t usually type-hinted, so this is more or less required.
Table definition
create schema howto;
create table howto.soft_deletes (
id integer not null generated by default as identity,
data text,
deleted_at timestamp with time zone,
constraint soft_pkey primary key (id)
);
Limiting the returned rows
We’ll create a subclass of gateways\PrimaryKeyTableGateway as the example table has a scalar primary key
and put it in app\howto\db namespace:
namespace app\howto\db;
use sad_spirit\pg_builder\Select;
use sad_spirit\pg_gateway\{
Fragment,
FragmentBuilder,
TableSelect,
gateways\PrimaryKeyTableGateway
};
class SoftDeletesGateway extends PrimaryKeyTableGateway
{
public function select(
iterable|FragmentBuilder|Fragment|\Closure|null $fragments = null,
array $parameters = []
): TableSelect {
return new TableSelect(
$this->tableLocator,
$this,
$this->convertFragments($fragments, $parameters),
function (): Select {
/** @var Select $select */
$select = $this->tableLocator->createFromString(\sprintf(
'select self.* from %s as self where self.deleted_at is null',
$this->getDefinition()->getName()
));
return $select;
},
function (): Select {
/** @var Select $select */
$select = $this->tableLocator->createFromString(\sprintf(
'select count(self.*) from %s as self where self.deleted_at is null',
$this->getDefinition()->getName()
));
return $select;
}
);
}
}
TableSelect constructor accepts closures that generate the base AST for SELECT / SELECT COUNT(*) statements,
here we are creating these from strings for readability.
Implementing soft deletes
We need to
Make sure that generated
UPDATEstatements work only with not-yet-deleted rows;Instead of
DELETEstatements generateUPDATEs settingdeleted_atto current time.
The first item is done the same way as for SELECT above, by replacing base AST used for UPDATE:
use sad_spirit\pg_builder\NativeStatement;
use sad_spirit\pg_builder\Update;
use sad_spirit\pg_gateway\FragmentList;
use sad_spirit\pg_gateway\StatementType;
public function createUpdateStatement(FragmentList $fragments): NativeStatement
{
return $this->tableLocator->createNativeStatementUsingCache(
function () use ($fragments): Update {
/** @var Update $update */
$update = $this->tableLocator->createFromString(\sprintf(
// update does not allow empty set clause, the fake one will be replaced
'update %s as self set foo = bar where self.deleted_at is null',
$this->getDefinition()->getName()
));
$fragments->applyTo($update);
return $update;
},
$this->generateStatementKey(StatementType::Update, $fragments)
);
}
The second one is done by “reimplementing” createDeleteStatement():
use sad_spirit\pg_gateway\Expression;
use sad_spirit\pg_gateway\fragments\SetClauseFragment;
public function createDeleteStatement(FragmentList $fragments): NativeStatement
{
$fragments->add(new SetClauseFragment(
$this->getDefinition()->getColumns(),
$this->tableLocator,
['deleted_at' => new Expression('now()')]
));
return $this->createUpdateStatement($fragments);
}
Here we are taking advantage of the fact that all Fragments that can be applied to DELETE can also
be applied to UPDATE. So we just add a new Fragment to the list that marks the row as deleted
and pass the list to the method creating an UPDATE.
Wrapping up
As with builders, we add an instance of NameMappingGatewayFactory to $locator:
$locator->addTableGatewayFactory(new NameMappingGatewayFactory([
'howto' => '\\app\\howto\\db'
]));
and add a directive to .phpstorm.meta.php
namespace PHPSTORM_META {
override(\sad_spirit\pg_gateway\TableLocator::createGateway(), map([
'howto.soft_deletes' => \app\howto\db\SoftDeletesGateway::class
]));
}
After that we can check that whatever is being generated looks good:
$gateway = $locator->createGateway('howto.soft_deletes');
echo $gateway->selectByPrimaryKey(1)
->createSelectStatement()
->getSql();
echo \PHP_EOL . \PHP_EOL;
echo $gateway->createUpdateStatement(new FragmentList(
$gateway->createPrimaryKey(1),
new SetClauseFragment(
$gateway->getDefinition()->getColumns(),
$locator,
['data' => 'some data']
)
))
->getSql();
echo \PHP_EOL . \PHP_EOL;
echo $gateway->createDeleteStatement(new FragmentList($gateway->createPrimaryKey(1)))
->getSql();
outputting
select self.*
from howto.soft_deletes as self
where self.deleted_at is null
and self.id = $1::int4
update howto.soft_deletes as self
set "data" = $1::"text"
where self.deleted_at is null
and self.id = $2::int4
update howto.soft_deletes as self
set deleted_at = now()
where self.deleted_at is null
and self.id = $1::int4