PHPackages                             check24/apitk-url-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. check24/apitk-url-bundle

ActiveSymfony-bundle[API Development](/categories/api)

check24/apitk-url-bundle
========================

This bundle provides filter, sorting and pagination for RESTful API's

3.0.3(3y ago)41.9k6[3 PRs](https://github.com/byWulf/apitk-url-bundle/pulls)MITPHPPHP ^7.4 || ^8.0

Since Jun 24Pushed 3y agoCompare

[ Source](https://github.com/byWulf/apitk-url-bundle)[ Packagist](https://packagist.org/packages/check24/apitk-url-bundle)[ RSS](/packages/check24-apitk-url-bundle/feed)WikiDiscussions master Synced 1mo ago

READMEChangelog (10)Dependencies (18)Versions (39)Used By (0)

apitk-url-bundle: Filter, Sorting and Pagination for RESTful API's
==================================================================

[](#apitk-url-bundle-filter-sorting-and-pagination-for-restful-apis)

Installation
------------

[](#installation)

Install the package via composer:

```
composer require check24/apitk-url-bundle

```

Usage
-----

[](#usage)

### Defining possible filers, sortings and pagination

[](#defining-possible-filers-sortings-and-pagination)

#### Filtering

[](#filtering)

You can specify in the annotations of the action, which fields should be filterable by the client:

```
use Shopping\ApiTKUrlBundle\Annotation as ApiTK;

/**
 * Returns the users in the system.
 *
 * @Rest\Get("/v1/users")
 * @Rest\View()
 *
 * @ApiTK\Filter(name="username")
 * @ApiTK\Filter(name="created", allowedComparisons={"gt","lt"})
 * @ApiTK\Filter(name="active", enum={"true","false"})
 * @ApiTK\Filter(name="country", queryBuilderName="a.country")
 *
 * @return User[]
 */

```

Limit the possibilities for the user with the `allowedComparisons` and `enum` option.

If you want to use the built in query builder applier and the entity field name differs from the filter field name (f.e. because it's a field from a joined tabled with an alias) use the `queryBuilderName` option.

The client can now call your API endpoint with filter options like `GET /v1/users?filter[created][gt]=2018-01-01&filter[country][in]=DE,AT`. If the client specified invalid filters (fields or comparisons you didn't configure/allow), the client will get a 400 response.

You can also use route parameters for filtering input. Just declare the filter with the same name the placeholder in the route is named:

```
/**
 * Returns the addresses for the given user.
 *
 * @Rest\Get("/v1/users/{id}/addresses")
 * @Rest\View()
 *
 * @ApiTK\Filter(name="id", queryBuilderName="u.id")
 *
 * @return Address[]
 */

```

#### Sorting

[](#sorting)

You can specify in the annotations of the action, which fields should be sortable by the client:

```
use Shopping\ApiTKUrlBundle\Annotation as ApiTK;

/**
 * Returns the users in the system.
 *
 * @Rest\Get("/v1/users")
 * @Rest\View()
 *
 * @ApiTK\Sort(name="username")
 * @ApiTK\Sort(name="zipcode", queryBuilderName="a.zipCode", allowedDirections={"asc"})
 *
 * @return User[]
 */

```

Limit the possibilities for the user with the `allowedDirections` option (f.e. if you only want to support ascending sorting).

If you want to use the built in query builder applier and the entity field name differs from the sort field name (f.e. because it's a field from a joined tabled with an alias) use the `queryBuilderName` option.

The client can now call your API endpoint with sort options like `GET /v1/users?sort[zipcode]=asc&sort[username]=desc`. If the client specified invalid sorts (fields or directions you didn't configure/allow), the client will get a 400 response.

#### Pagination

[](#pagination)

You can specify in the annotations of the action, if the result should be paginatable by the client:

```
use Shopping\ApiTKUrlBundle\Annotation as ApiTK;

/**
 * Returns the users in the system.
 *
 * @Rest\Get("/v1/users")
 * @Rest\View()
 *
 * @ApiTK\Pagination
 * Or:
 * @ApiTK\Pagination(maxEntries=25)
 *
 * @return User[]
 */

```

If you want to limit the client, how many items per page he gets, you can specify the `maxEntries` option. But please only add one `Pagination` annotation (not like in the example above).

The client can now call your API endpoint with the limit option like `GET /v1/users?limit=10` (get the first 10 entries) or `GET /v1/users?limit=30,10` (get the 10 entries with an offset of 30 (=4th page)). If the client tries to paginate when it isn't enabled by you or he wants to get more items per page than specified by you, the client will get a 400 response.

Also a new header `x-apitk-pagination-total` is sent in the response containing the total amount of entries, so the client can adjust his pagination buttons.

### Accessing client input

[](#accessing-client-input)

#### Autoloading array through param converter

[](#autoloading-array-through-param-converter)

You can automatically get the entity result array of all your filters in the correct order and pagination subset easily injected into your action.

First set your default repository in your `doctrine.yaml` to our `ApiToolkitRepository`:

```
doctrine:
  orm:
    default_repository_class: Shopping\ApiTKUrlBundle\Repository\ApiToolkitRepository
```

For all your custom repositories, extend them from the `ApiToolkitRepository` (or if you want them to be injectable, from `ApiToolkitServiceRepository`, which extends from the `ServiceEntityRepository`, so be sure to implement the constructor the right way) so they also get the needed functionality.

After that, add a `@ApiTK\Result` annotation to your controller action. The action parameter will automatically gets filled with the filtered, sorted and paginated result set of the given entity's repository:

```
//ItemController.php
/**
 * @ApiTK\Filter(name="name")
 * @ApiTK\Sort(name="name")
 * @ApiTK\Pagination
 *
 * @ApiTK\Result("items", entity="App\Entity\Item")
 */
public function getItems(array $items)
{
    return $items;
}

```

If you need to filter/sort for fields in a joined entity, just define your own `findByRequest()` method in the custom entity's repository:

```
//UserRepository.php
use Shopping\ApiTKUrlBundle\Repository\ApiToolkitRepository;
class UserRepository extends ApiToolkitRepository
{
    public function findByRequest(ApiService $apiService): array
    {
        $qb = $this->createQueryBuilder('u');
        $qb->leftJoin('u.addresses', 'a')->distinct();

        $apiService->applyToQueryBuilder($qb);

        return $qb->getQuery()->getResult();
    }
}

```

```
//UserController.php
/**
 * @ApiTK\Filter(name="username")
 * @ApiTK\Filter(name="country", queryBuilderName="a.country")
 * @ApiTK\Pagination
 *
 * @ApiTK\Result("users", entity="App\Entity\User")
 */
public function getUsers(array $users)
{
    return $users;
}

```

If you need to add different methods to your repository, that can be executed by the `Result` annotation, than you can add the `methodName="findBySomethingElse"` parameter to your annotation. It will then look for this method in your repository instead of the default `findByResult()` method. Be sure to accept as the sole parameter the `ApiService`.

As a result, this is also possible:

```
//UserRepository.php
use Shopping\ApiTKUrlBundle\Repository\ApiToolkitRepository;
class UserRepository extends ApiToolkitRepository
{
    public function findBarBaz(ApiService $apiService): array
    {
        // your own logic
    }
}

```

```
//UserController.php
/**
 * @ApiTK\Result("users", entity="App\Entity\User", methodName="findBarBaz")
 */
public function getUsers(array $users)
{
    return $users;
}

```

Note: if the paginator was enabled in your annotations, the query will get executed by the `applyToQueryBuilder()` method in your repository to determine the total count. Be sure to call the method at the end, after building the rest of your query.

You can specify the entity manager by adding a `entityManager="foobar"` to your annotation, if you need to use another entity manager / connection than the default one.

#### Manually accessing

[](#manually-accessing)

If you have to implement custom logic with filtering, sorting and pagination, you can also inject the `ApiService` and use its methods:

```
//UserController.php
public function getUsersV1(EntityManagerInterface $entityManager, ApiService $apiService)
{
    $users = $entityManager->getRepository(User::class)->findAll();

    //Filtering
    if ($apiService->hasFilteredField('username')) {
        $usernameFilter = $apiService->getFilteredField('username');
        $users = array_filter($users, function($user) use ($usernameFilter) {
            if ($usernameFilter->getComparison() === ApiTK\Filter::COMPARISON_EQUALS) {
                return $user->getUsername() === $usernameFilter->getValue();
            }
            return false;
        });
    }
    /*...*/

    //Sorting
    foreach (array_reverse($apiService->getSortedFields()) as $sortField) {
        if ($sortField->getName() === 'username') {
            usort($users, function($user1, $user2) use ($sortField) {
                if ($sortField->getDirection === ApiTK\Sort::ASCENDING) {
                    return $user1->getUsername()  $user2->getUsername();
                } else {
                    return $user2->getUsername()  $user1->getUsername();
                }
            });
        }
        /*...*/
    }

    //Pagination
    $apiService->setPaginationTotal(count($users));
    $users = array_slice($users, $apiService->getPaginationOffset(), $apiService->getPaginationLimit());

    return $users;
}

```

#### Implementing only some filters manually

[](#implementing-only-some-filters-manually)

In case you have some "virtual" fields that need some custom logic, you can use applyToQueryBuilder and
still define your field by hand.

Let's say you want to implement a search parameter. This search looks into username and email.

```
//UserController.php
/**
 * @ApiTK\Filter(name="search", autoApply=false)
 *
 * @ApiTK\Result("users", entity="App\Entity\User")
 */
public function getUsers(array $users)
{
    return $users;
}

```

For your parameter to be available, you need to register a new `Filter` field. It is required to set `autoApply=false` for this filter, because there's no "search" field on the Entity and we want to assemble this part of the query on our own.

```
//UserRepository.php
use Shopping\ApiTKUrlBundle\Repository\ApiToolkitRepository;
class UserRepository extends ApiToolkitRepository
{
    public function findByRequest(ApiService $apiService): array
    {
        $qb = $this->createQueryBuilder('u');

        if ($apiService->hasFilteredField('search')) {
            $search = $apiService->getFilteredField('search');
            $qb->andWhere(
                $qb->expr()->orX(
                    $qb->expr()->like('u.username', ':query'),
                    $qb->expr()->like('u.email', ':query')
                )
            )->setParameter('query', '%' . $search . '%');
        }

        $apiService->applyToQueryBuilder($qb);

        return $qb->getQuery()->getResult();
    }
}

```

`applyToQueryBuilder` will skip our `autoApply=false` field, so we can add it ourselves.

Important: When using also the paginator, call the `$apiService->applyToQueryBuilder()` method AFTER your manual filtering, so the paginator can build the total count header on the filtered result. Otherwise you get incorrect values in your header.

#### Implementing a sort manually

[](#implementing-a-sort-manually)

Same as with manual filter properties, you can implement manual sorting like shown above:

```
//UserController.php
/**
 * @ApiTK\Sort(name="mySortProperty", autoApply=false)
 *
 * @ApiTK\Result("users", entity="App\Entity\User")
 */
public function getUsers(array $users)
{
    return $users;
}

```

```
//UserRepository.php
use Shopping\ApiTKUrlBundle\Repository\ApiToolkitRepository;
class UserRepository extends ApiToolkitRepository
{
    public function findByRequest(ApiService $apiService): array
    {
        $qb = $this->createQueryBuilder('u');

        $apiService->applyToQueryBuilder($qb);

        if ($apiService->hasSortedField('mySortProperty')) {
            // perform your own sorting logic for this field
        }

        return $qb->getQuery()->getResult();
    }
}

```

For more advanced purposes, see refer to the "Manually accessing" section above.

### Documentation

[](#documentation)

The defined filers, sorts and pagination will automatically get added to the NelmioApiDoc output (aka Swagger UI). You don't have to worry about that.

###  Health Score

38

—

LowBetter than 84% of packages

Maintenance20

Infrequent updates — may be unmaintained

Popularity22

Limited adoption so far

Community19

Small or concentrated contributor base

Maturity79

Established project with proven stability

 Bus Factor1

Top contributor holds 62.6% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~53 days

Recently: every ~76 days

Total

29

Last Release

1381d ago

Major Versions

0.9.0 → 1.0.02018-08-16

1.0.15 → 2.0.02020-03-23

2.3.0 → 3.0.02021-10-13

PHP version history (3 changes)0.1.0PHP ^7.1.3

2.2.0PHP ^7.1.3 || ^8.0

3.0.0PHP ^7.4 || ^8.0

### Community

Maintainers

![](https://www.gravatar.com/avatar/caaeaf0a1a74fef634f9eea17d585684d3758882c622ac42f8ef6973b1ef1910?d=identicon)[byWulf](/maintainers/byWulf)

![](https://www.gravatar.com/avatar/82e22e3d09adca0634ae909f6e3a8cbb82e193d6436ca7e304e2ff666097e58a?d=identicon)[ofeige](/maintainers/ofeige)

![](https://www.gravatar.com/avatar/f94057f8465d9062e73c8a219f4edb5b36612d624db53240b9c2c7b6616b6c55?d=identicon)[alexdo](/maintainers/alexdo)

---

Top Contributors

[![byWulf](https://avatars.githubusercontent.com/u/135651?v=4)](https://github.com/byWulf "byWulf (97 commits)")[![alexdo](https://avatars.githubusercontent.com/u/690188?v=4)](https://github.com/alexdo "alexdo (27 commits)")[![markuspoerschke](https://avatars.githubusercontent.com/u/1222377?v=4)](https://github.com/markuspoerschke "markuspoerschke (11 commits)")[![dariotilgner](https://avatars.githubusercontent.com/u/1674931?v=4)](https://github.com/dariotilgner "dariotilgner (6 commits)")[![ofeige](https://avatars.githubusercontent.com/u/4577803?v=4)](https://github.com/ofeige "ofeige (4 commits)")[![taieb-b](https://avatars.githubusercontent.com/u/102976733?v=4)](https://github.com/taieb-b "taieb-b (3 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (3 commits)")[![manjencic](https://avatars.githubusercontent.com/u/3406389?v=4)](https://github.com/manjencic "manjencic (3 commits)")[![arthurk-c24](https://avatars.githubusercontent.com/u/170720004?v=4)](https://github.com/arthurk-c24 "arthurk-c24 (1 commits)")

###  Code Quality

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/check24-apitk-url-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/check24-apitk-url-bundle/health.svg)](https://phpackages.com/packages/check24-apitk-url-bundle)
```

###  Alternatives

[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.4k5.6M647](/packages/sylius-sylius)[ec-cube/ec-cube

EC-CUBE EC open platform.

78527.0k1](/packages/ec-cube-ec-cube)[sulu/headless-bundle

Bundle that provides controllers and services for using Sulu as headless content management system

55133.7k2](/packages/sulu-headless-bundle)[open-dxp/opendxp

Content &amp; Product Management Framework (CMS/PIM)

7310.3k29](/packages/open-dxp-opendxp)[webfactory/legacy-integration-bundle

A battle-proven approach to facilitate the incremental migration of legacy applications to the Symfony2 stack.

1611.4k](/packages/webfactory-legacy-integration-bundle)[chameleon-system/chameleon-base

The Chameleon System core.

1026.5k3](/packages/chameleon-system-chameleon-base)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
