Filters Pattern in Nette Database

This post is deprecated since January 2017
I have deprecated this package, because it was not very active - it has been downloaded only 5 times during past 4 months.

You want to delete comments, so your readers won't see any spam or violent content. But you want to see them in administration. So you would have to create 2 different methods. Today I will show you, how to make only single one.

Current way to do this

Let's say we have a CommentRepository class, where we put all methods that work with "comment" table.

In it, we have 2 methods:

namespace App\Repository;

use Nette\Database\Context;
use Nette\Database\Table\Selection;

class CommentRepository
{

    /**
     * @var Selection
     */
    private $commentTable;

    public function __construct(Context $database)
    {
        $this->commentTable = $database->table('comment');
    }

    /**
     * Returns only comments, that are not deleted.
     */
    public function fetchCommentsForFrontend()
    {
        return $this->commentTable->where('is_deleted = ?', FALSE)
            ->fetchAll();
    }

    public function fetchCommentsForAdministration()
    {
        return $this->commentTable->fetchAll();
    }

}

And decide manually, where to use fetchCommentsForFrontend() and where to use fetchAllCommentsForAdministration().

This approach is bad practise, because it will eventually make your every repository class double its size.

No need for that! This has been already solved somewhere else.

Do you know Doctrine Filters? No? Go check this short article to get the clue. I'll wait here.

Soft delete filter - in theory

In short, with filters you can modify any query. In our case:

This will influence every query for "comment" table. So you can be sure you'll never forget to add the condition.

Show me the code

There is not much to talk about, because filters are made to be simple. So here is filter:

# app/Database/Filter/SoftDeletableFilter.php

namespace App\Database\Filter;

use Nette\Application\Application;
use Nette\Database\Table\Selection;
use Zenify\NetteDatabaseFilters\Contract\FilterInterface;

class SoftDeletableFilter implements FilterInterface
{

    public function __construct(Application $application)
    {
        $this->application = $application;
    }

    public function applyFilter(Selection $selection)
    {
        // 1. apply only to "comment" table
        $tableName = $selection->getName();
        if ($tableName !== 'comment') {
            return;
        }

        // 2. skip for admin presenters
        // add your custom method, that detects admin presenter via name or class inheritance
        if ($this->isAdminPresenter($this->application->getPresenter())) {
            return;
        }

        // 3. show only visible (not deleted) comments
        $selection->where('is_deleted = ?', FALSE);
    }

}

And that's all.

These filters are possible in Nette\Database only thanks to Zenify/NetteDatabaseFilters package.

Do you want to try it for yourself? Let's go.

Your First Filter in 4 steps

1. Install package

composer require zenify/nette-database-filters

2. Register Extension

# app/config/config.neon
extensions:
    - Zenify\NetteDatabaseFilters\DI\NetteDatabaseFiltersExtension

3. Create your filter

The one above...

4. Register it as a service

# app/config/config.neon
services:
    - App\Database\Filter\SoftDeletableFilter

And that's it! Now your filter will be reflected in whole application.

So you can reduce your repository code and use fetchComments() in all places.

# app/Repository/CommentRepository.php

namespace App\Repository;

use Nette\Database\Context;
use Nette\Database\Table\Selection;

class CommentRepository
{

    /**
     * @var Selection
     */
    private $commentTable;

    public function __construct(Context $database)
    {
        $this->commentTable = $database->table('comment');
    }

    public function fetchComments()
    {
        return $this->commentTable->fetchAll();
    }

}

For further use just check Readme for Zenify/NetteDatabaseFilters.

Protip for multiple tables with the same column!

What if you have multiple tables with "is_deleted" column? "comment", "article", "page" table... maybe "banner", "user" in the furture.

And I will show you how do it:

# app/Database/Filter/SoftDeletableFilter.php

// ...

public function applyFilter(Selection $selection)
{
    if (!$this->isSoftdelable($selection)) {
        return;
    }

    // ... condition code
}

/**
 * @return bool
 */
private function isSoftdelable(Selection $selection)
{
    $selectionToCheck = clone $selection;
    return $selectionToCheck->fetch()
        ->offsetExists('is_deleted');
}

Pretty neat, huh?

What Have You Learned Today?

If you have some tips how to this simpler or want to share your experience with filters, just let me know below.

Happy coding!