Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

TableGateway and a model class

A TableGateway is an object from laminas-db that can select, insert, update and delete rows of one table. You wrap it in a class of your own with clear methods — find(), save() — and the controller works with that class. Data classes live in module/<Module>/src/Model.

The row class

One object of this class is one row of the posts table. Create the file by hand:

// module/Blog/src/Model/Post.php
namespace Blog\Model;
 
class Post
{
    public ?int $id = null;
    public string $title = '';
    public string $body = '';
 
    public function exchangeArray(array $data): void
    {
        $this->id = isset($data['id']) ? (int) $data['id'] : null;
        $this->title = $data['title'] ?? '';
        $this->body = $data['body'] ?? '';
    }
}

The exchangeArray() method fills the object from an array — a table row. Laminas calls it for every row it finds.

The table class

// module/Blog/src/Model/PostTable.php
namespace Blog\Model;
 
use Laminas\Db\TableGateway\TableGateway;
 
class PostTable
{
    public function __construct(private TableGateway $gateway) {}
 
    public function fetchAll(): array
    {
        return iterator_to_array($this->gateway->select());
    }
 
    public function find(int $id): ?Post
    {
        return $this->gateway->select(['id' => $id])->current() ?: null;
    }
}

select() without arguments returns all rows, and with an array — the rows where the column equals the value. Values from the array are sent to the database as parameters, so there is no need to paste them into SQL.

The factory

The gateway needs a table name and the adapter — these are not typed services, so the factory is written by hand:

// module/Blog/src/Model/PostTableFactory.php
namespace Blog\Model;
 
use Laminas\Db\Adapter\AdapterInterface;
use Laminas\Db\ResultSet\ResultSet;
use Laminas\Db\TableGateway\TableGateway;
use Psr\Container\ContainerInterface;
 
class PostTableFactory
{
    public function __invoke(ContainerInterface $container): PostTable
    {
        $resultSet = new ResultSet();
        $resultSet->setArrayObjectPrototype(new Post());
 
        $gateway = new TableGateway('posts', $container->get(AdapterInterface::class), null, $resultSet);
 
        return new PostTable($gateway);
    }
}

A ResultSet with the new Post() prototype turns every result row into a Post object. Register the class in module/Blog/config/module.config.php:

'service_manager' => [
    'factories' => [
        Model\PostTable::class => Model\PostTableFactory::class,
    ],
],

Calling it from the controller

// module/Blog/src/Controller/PostController.php
use Blog\Model\PostTable;
 
public function __construct(private PostTable $posts) {}
 
public function indexAction()
{
    return new ViewModel(['posts' => $this->posts->fetchAll()]);
}
 
public function viewAction()
{
    $post = $this->posts->find((int) $this->params()->fromRoute('id'));
 
    if ($post === null) {
        return $this->notFoundAction();
    }
 
    return new ViewModel(['post' => $post]);
}

The controller is registered with ReflectionBasedAbstractFactory, so PostTable reaches the constructor without any config change. Posts are objects now: in view scripts write $post->title instead of $post['title']. Open /blog — the list comes from the database.

Insert, update and delete

Add two more methods to PostTable:

public function save(Post $post): void
{
    $data = ['title' => $post->title, 'body' => $post->body];
 
    if ($post->id === null) {
        $this->gateway->insert($data);
        $post->id = (int) $this->gateway->getLastInsertValue();
 
        return;
    }
 
    $this->gateway->update($data, ['id' => $post->id]);
}
 
public function delete(int $id): void
{
    $this->gateway->delete(['id' => $id]);
}

A new post has an empty id, so insert() runs; otherwise update() does. The second argument of update() and the argument of delete() are the condition that selects the rows to touch.

Common mistake

Calling $this->gateway->update($data) without the second argument changes every row of the table, and delete([]) with an empty condition removes them all. Always pass a condition with the id and make sure the id is not null.

Thanks for reading!

Donate to the site — we work hard for you.