PHPackages                             tyloo/atc - 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. [Testing &amp; Quality](/categories/testing)
4. /
5. tyloo/atc

ActiveLibrary[Testing &amp; Quality](/categories/testing)

tyloo/atc
=========

Fluent API testing for Symfony with JSON Schema validation, container mocking, and in-memory infrastructure swaps.

0.1.0(4w ago)02MITPHPPHP &gt;=8.3CI passing

Since May 12Pushed 4w agoCompare

[ Source](https://github.com/tyloo/atc)[ Packagist](https://packagist.org/packages/tyloo/atc)[ RSS](/packages/tyloo-atc/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (16)Versions (2)Used By (0)

 [![ATC - APITestCase](social.png)](social.png)

ATC - APITestCase
=================

[](#atc---apitestcase)

 A fluent, batteries-included testing layer for Symfony JSON APIs.

 [![Latest version](https://camo.githubusercontent.com/0d7234f278dd7807e3ef0c36c6fadad65b382868e8fd517c61947cb87e60d36d/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f74796c6f6f2f6174632e737667)](https://packagist.org/packages/tyloo/atc) [![Downloads](https://camo.githubusercontent.com/531f725284e0deb6d2433b254dc57797908379b66f940907bb812b210938883d/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f74796c6f6f2f6174632e737667)](https://packagist.org/packages/tyloo/atc) [![CI](https://github.com/tyloo/atc/actions/workflows/ci.yaml/badge.svg)](https://github.com/tyloo/atc/actions/workflows/ci.yaml) [![Coverage](https://camo.githubusercontent.com/4ae6ee366b223bdd44cc03f4df6697db9665a82f14ab7644a78daf46e89e2c12/68747470733a2f2f636f6465636f762e696f2f67682f74796c6f6f2f6174632f6272616e63682f6d61696e2f67726170682f62616467652e737667)](https://codecov.io/gh/tyloo/atc) [![License](https://camo.githubusercontent.com/bb14336ee0238a37024c805bb4b40460189ea3818aa9135412d98e0bfd708fa8/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f74796c6f6f2f6174632e737667)](LICENSE) [![PHP version](https://camo.githubusercontent.com/02d20fa3567fdbd60bb4856cc9b51b2136dd6faf763d037d04f7c586267153a5/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f74796c6f6f2f6174632e737667)](https://camo.githubusercontent.com/02d20fa3567fdbd60bb4856cc9b51b2136dd6faf763d037d04f7c586267153a5/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f74796c6f6f2f6174632e737667) [![Symfony 6.4 | 7 | 8](https://camo.githubusercontent.com/2cc87ac5f5439a115f9a28d838196909bd7bead287f6950afcca003d56fc521a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53796d666f6e792d362e3425323025374325323037253230253743253230382d626c61636b3f6c6f676f3d73796d666f6e79)](https://camo.githubusercontent.com/2cc87ac5f5439a115f9a28d838196909bd7bead287f6950afcca003d56fc521a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53796d666f6e792d362e3425323025374325323037253230253743253230382d626c61636b3f6c6f676f3d73796d666f6e79)

---

**Fluent API testing for Symfony, with zero boilerplate.** ATC is a batteries-included testing layer on top of `WebTestCase`: chained HTTP+JSON assertions, JSON Schema and JMESPath, container-aware mocking, profiler-backed N+1 detection, and ready-to-use in-memory swaps for Messenger, Mailer, Notifier, HTTP client, and Cache.

Contents
--------

[](#contents)

- [Installation](#installation)
- [Quick start](#quick-start)
- [Issuing requests](#issuing-requests)
- [Asserting on responses](#asserting-on-responses)
- [JMESPath assertions](#jmespath-assertions)
- [JSON Schema validation](#json-schema-validation)
- [Performance assertions](#performance-assertions)
- [Authentication](#authentication)
- [Container mocking](#container-mocking)
- [Database](#database)
- [Messenger](#messenger)
- [Mailer](#mailer)
- [Notifier](#notifier)
- [HTTP client](#http-client)
- [Cache](#cache)
- [Profiler &amp; N+1 detection](#profiler--n1-detection)
- [Customization](#customization)
- [Compatibility](#compatibility)
- [Recommended companions](#recommended-companions)

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

[](#installation)

```
composer require --dev tyloo/atc
```

That is it. No bundle to register, no YAML to write. Extend `Tyloo\Atc\ApiTestCase` in any functional test and you have the full surface. Sensible defaults out of the box:

- `tests/Schemas/` for JSON Schema files
- No default headers, no default mocks
- In-memory transports auto-discovered from your `framework.messenger` config
- HTTP client mock is strict (unmatched outbound requests fail the test)

Each default has a protected override hook on the test case (see [Customization](#customization)).

Quick start
-----------

[](#quick-start)

A realistic scenario:

- Spin up an authenticated admin with [Zenstruck Foundry](https://github.com/zenstruck/foundry)
- Validate the response against a JSON Schema
- Assert the welcome email got queued
- Confirm the row landed in the database

```
final class CreateUserTest extends ApiTestCase
{
    use Factories;
    use ResetDatabase;
    use InteractsWithDatabase;
    use InteractsWithMessenger;

    #[Test]
    public function admin_creates_a_user_and_queues_welcome_email(): void
    {
        $admin = AdminFactory::createOne();

        $this->actingAs($admin)
            ->post('/api/users', json: [
                'email' => 'jean@bond.com',
                'name'  => 'Jean Bond',
            ])
            ->assertStatus(201)
            ->assertMatchesJsonSchema('users/create.json')
            ->assertJsonPath('data.email', 'jean@bond.com')
            ->assertHeader('Location', '/api/users/42');

        $this->assertDatabaseHas(User::class, ['email' => 'jean@bond.com']);
        $this->assertMessageDispatched(
            SendWelcomeEmail::class,
            fn (SendWelcomeEmail $m) => $m->email === 'jean@bond.com',
        );
    }
}
```

One method, no setup boilerplate. The rest of this README walks through every feature in detail.

Issuing requests
----------------

[](#issuing-requests)

All HTTP verbs are available on the test case via `InteractsWithApi` (auto-loaded in `ApiTestCase`):

```
$this->get('/api/users');
$this->post('/api/users', json: ['name' => 'Alice']);
$this->patch('/api/users/1', json: ['name' => 'Bob']);
$this->put('/api/users/1', json: [...]);
$this->delete('/api/users/1');
```

Headers and query strings are first-class arguments:

```
$this->get('/api/users',
    headers: ['X-Tenant' => 'acme'],
    query:   ['filter' => 'active', 'page' => 2],
);
```

Form payloads and file uploads:

```
$this->post('/api/login', formData: ['username' => 'a', 'password' => 'b']);
$this->post('/api/uploads', files: ['file' => $uploadedFile]);
```

When `json:` is provided it takes precedence; `formData` is ignored. `Content-Type: application/json` is set automatically.

Persist headers across multiple requests in the same test:

```
$this->withHeaders(['Accept-Language' => 'fr'])
    ->get('/api/users')
    ->assertJsonContains(['greeting' => 'Bonjour']);

$this->get('/api/products')->assertStatusOk(); // still sends Accept-Language: fr
```

Asserting on responses
----------------------

[](#asserting-on-responses)

Every verb call returns an `ApiResponse` that you can chain assertions on:

```
$this->get('/api/users/42')
    ->assertStatusOk()                          // 200
    ->assertHeader('Content-Type', 'application/json')
    ->assertJsonContains(['id' => 42, 'name' => 'Alice'])
    ->assertJsonPath('roles[0]', 'admin');
```

### Status

[](#status)

```
$response->assertStatus(200); // exact match
$response->assertStatusOk();  // shorthand for the common case
```

### Headers

[](#headers)

```
$response->assertHeader('Content-Type', 'application/json');
$response->assertHeaderHas('ETag');         // present, value irrelevant
$response->assertHeaderMissing('X-Debug-Token');
```

### JSON body, exact match

[](#json-body-exact-match)

```
$response->assertJson([
    'id'   => 1,
    'name' => 'Alice',
]);
```

Order and types must match. Use `assertJsonContains` when you only care about a subset.

### JSON body, subset match

[](#json-body-subset-match)

```
$response->assertJsonContains([
    'data' => ['email' => 'alice@example.com'],
]);
```

Recursive: nested arrays only need to contain the listed keys.

### Raw access

[](#raw-access)

When the assertion helpers aren't enough, grab the decoded body directly:

```
$body = $response->json();                  // decoded array
$email = $response->json('data.email');     // JMESPath expression

$status = $response->statusCode();          // int
$body   = $response->content();             // raw string
$ms     = $response->responseTimeMs();      // float
$raw    = $response->raw();                 // Symfony Response

$response = $this->lastResponse();          // last response from this test
```

JMESPath assertions
-------------------

[](#jmespath-assertions)

ATC uses [JMESPath](https://jmespath.org) for navigating JSON responses (powered by `mtdowling/jmespath.php`):

```
$response
    ->assertJsonPath('user.email', 'alice@example.com')
    ->assertJsonPath('data[0].active', true)
    ->assertJsonPath('roles | length(@)', 3);
```

Pass a callable to assert with a predicate (truthy = pass):

```
$response->assertJsonPath('id', fn ($v) => is_int($v) && $v > 0);
```

Assert that a path **does not** resolve to a value, or count items at a path:

```
$response->assertJsonMissingPath('deleted_at');
$response->assertJsonCount(3, 'data');
$response->assertJsonCount(2);              // root must be an array of 2
```

JSON Schema validation
----------------------

[](#json-schema-validation)

Drop a JSON Schema file under `tests/Schemas/` (configurable; see [Customization](#customization)) and validate the response shape against it:

```
// tests/Schemas/users/create.json
{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "required": ["data"],
    "properties": {
        "data": {
            "type": "object",
            "required": ["id", "email", "name"],
            "properties": {
                "id":    { "type": "integer" },
                "email": { "type": "string", "format": "email" },
                "name":  { "type": "string" }
            }
        }
    }
}
```

```
$this->post('/api/users', json: [...])
    ->assertStatus(201)
    ->assertMatchesJsonSchema('users/create.json');
```

Powered by `justinrainbow/json-schema`. Failures include the schema path and a human-readable list of validation errors.

Performance assertions
----------------------

[](#performance-assertions)

Wall-clock duration of each request is measured automatically:

```
$this->get('/api/heavy-report')
    ->assertStatusOk()
    ->assertResponseTimeLessThan(500)       // < 500 ms
    ->assertResponseTimeBetween(50, 500);   // sanity bounds (catch suspiciously-fast cached responses)
```

Tip: use generous bounds in CI to avoid flakiness.

Authentication
--------------

[](#authentication)

ATC targets stateless / token-based APIs, so authentication is just "attach the right header to the next request".

### Raw tokens

[](#raw-tokens)

```
$this->withToken('eyJhbGciOi...')->get('/api/me')->assertStatusOk();

// custom scheme
$this->withToken('xxx', scheme: 'Basic')->get('/api/me');
```

### `actingAs($user)` — paired with Foundry

[](#actingasuser--paired-with-foundry)

By default, `actingAs($user)` looks for a `getApiToken(): string` method on the user object and attaches the result as a Bearer token. Perfect when your `User` entity already exposes an API token.

The recommended pattern: build the user with [Zenstruck Foundry](https://github.com/zenstruck/foundry), then pass it straight to `actingAs()`:

```
use App\Factory\UserFactory;
use Tyloo\Atc\ApiTestCase;
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase;

final class ProfileTest extends ApiTestCase
{
    use Factories;
    use ResetDatabase;

    #[Test]
    public function returns_authenticated_users_profile(): void
    {
        $alice = UserFactory::createOne(['email' => 'alice@example.com']);

        $this->actingAs($alice)
            ->get('/api/me')
            ->assertStatusOk()
            ->assertJsonPath('email', 'alice@example.com');
    }
}
```

Your `User` entity (or its Foundry factory) just needs a `getApiToken(): string` accessor. Foundry handles persistence, `actingAs()` handles the Bearer header, ATC handles the rest.

### Custom auth strategy

[](#custom-auth-strategy)

Override `authenticate()` in your base test case to plug in any strategy (JWT, HMAC signature, opaque token, anything that maps `user → request credentials`):

```
use Tyloo\Atc\ApiTestCase;
use Tyloo\Atc\Http\ApiClient;

abstract class BaseApiTestCase extends ApiTestCase
{
    #[\Override]
    protected function authenticate(object $user, ApiClient $client): ApiClient
    {
        $jwt = static::getContainer()->get(JWTTokenManagerInterface::class);

        return $client->withToken($jwt->create($user));
    }
}
```

`actingAs($user)` will then route through your override across every test that extends `BaseApiTestCase`. No registry, no `$using:` argument, no bundle config — one method, one strategy per test suite.

Container mocking
-----------------

[](#container-mocking)

`InteractsWithContainer` lets you swap services for test doubles without touching `services.yaml`.

### Full mock

[](#full-mock)

```
$shopify = $this->mockService(ShopifyService::class);
$shopify->expects(self::once())
    ->method('createCustomer')
    ->willReturn('cust_123');

$this->post('/api/customers', json: ['email' => 'a@b.c'])->assertStatus(201);
```

### Partial mock (real behavior on un-listed methods)

[](#partial-mock-real-behavior-on-un-listed-methods)

```
$notifier = $this->partialMockService(NotifierService::class, ['send']);
$notifier->method('send')->willReturn(null);
// every other method on NotifierService keeps its real implementation.
```

Partial mocks require a concrete class.

### Inject any object as a service

[](#inject-any-object-as-a-service)

```
$this->setService('app.feature_flags', new InMemoryFeatureFlags(['beta' => true]));
```

### Default mocks for the whole test suite

[](#default-mocks-for-the-whole-test-suite)

Centralize "always-mocked-in-tests" services in a base test case:

```
abstract class BaseApiTestCase extends ApiTestCase
{
    protected function defaultMocks(): array
    {
        return [
            ShopifyService::class => fn () => $this->createMock(ShopifyService::class),
        ];
    }
}
```

Database
--------

[](#database)

Add the trait. ATC does not manage database lifecycle, so pair it with [Zenstruck Foundry](https://github.com/zenstruck/foundry) or `DAMA/DoctrineTestBundle`:

```
final class CreateUserTest extends ApiTestCase
{
    use Factories;
    use ResetDatabase;
    use InteractsWithDatabase;

    #[Test]
    public function admin_creates_user_persists_row(): void
    {
        $admin = UserFactory::createOne(['role' => 'admin']);

        $this->actingAs($admin)
            ->post('/api/users', json: ['email' => 'new@example.com'])
            ->assertStatus(201);

        $this->assertDatabaseHas(User::class, ['email' => 'new@example.com']);
        $this->assertDatabaseMissing(User::class, ['email' => 'deleted@example.com']);
        $this->assertDatabaseCount(User::class, 2);
    }
}
```

Messenger
---------

[](#messenger)

`InteractsWithMessenger` discovers `in-memory://` transports and lets you inspect dispatched messages. Handlers do **not** auto-execute, so you assert on the dispatch itself:

```
use Tyloo\Atc\Trait\InteractsWithMessenger;

final class BulkImportTest extends ApiTestCase
{
    use InteractsWithMessenger;

    #[\PHPUnit\Framework\Attributes\Test]
    public function csv_upload_queues_one_message_per_row(): void
    {
        $this->post('/api/imports', files: ['csv' => $this->uploadCsv('100-rows.csv')])
            ->assertStatus(202);

        $this->assertMessagesDispatchedCount(100, ImportRow::class);
        $this->assertMessageDispatched(
            ImportRow::class,
            fn (ImportRow $row) => $row->email === 'first@example.com',
        );
    }
}
```

Other helpers:

```
$this->assertNoMessagesDispatched();                    // any class
$this->assertNoMessagesDispatched(SendEmail::class);    // class-specific

$all     = $this->dispatchedMessages();                 // list
$welcomes = $this->dispatchedMessages(SendWelcomeEmail::class);
```

Mailer
------

[](#mailer)

`InteractsWithMailer` swaps the real Mailer for an in-memory capture and exposes assertions on sent emails:

```
use Tyloo\Atc\Trait\InteractsWithMailer;

final class PasswordResetTest extends ApiTestCase
{
    use InteractsWithMailer;

    #[\PHPUnit\Framework\Attributes\Test]
    public function reset_request_sends_a_one_time_link(): void
    {
        $this->post('/api/password/reset', json: ['email' => 'alice@example.com'])
            ->assertStatusOk();

        $this->assertEmailSent();
        $this->assertEmailSentTo(
            'alice@example.com',
            fn (Email $email) => str_contains((string) $email->getSubject(), 'Reset your password'),
        );
        $this->assertNoEmailsSent(); // for negative paths
    }
}
```

Notifier
--------

[](#notifier)

`InteractsWithNotifier` captures Symfony Notifier sends:

```
use Tyloo\Atc\Trait\InteractsWithNotifier;

final class OutageAlertTest extends ApiTestCase
{
    use InteractsWithNotifier;

    #[\PHPUnit\Framework\Attributes\Test]
    public function downstream_error_pages_the_oncall(): void
    {
        $this->mockService(StatusPageClient::class)
            ->method('latest')
            ->willThrowException(new \RuntimeException('upstream down'));

        $this->get('/api/health')->assertStatus(503);

        $this->assertNotificationSent();
        $this->assertSame(1, $this->sentNotifications()->count());
    }
}
```

HTTP client
-----------

[](#http-client)

`InteractsWithHttpClient` swaps the Symfony HTTP client for a `MockHttpClient` you control, and records every outbound request:

```
use Symfony\Component\HttpClient\Response\MockResponse;
use Tyloo\Atc\Trait\InteractsWithHttpClient;

final class GeocodingTest extends ApiTestCase
{
    use InteractsWithHttpClient;

    #[\PHPUnit\Framework\Attributes\Test]
    public function address_lookup_calls_geocoder_with_signed_query(): void
    {
        $this->mockHttpClient([
            new MockResponse(json_encode(['lat' => 48.85, 'lng' => 2.35]), ['http_code' => 200]),
        ]);

        $this->post('/api/addresses', json: ['street' => '1 rue de Rivoli'])
            ->assertStatus(201)
            ->assertJsonPath('lat', 48.85);

        $this->assertHttpRequestSent('GET', 'https://api.example.com/geocode');
    }
}
```

By default the mock is strict: an unmatched request fails the test. Override `resolveHttpClientStrict()` in your test case to return `false` if you'd rather let unmatched requests pass through.

Cache
-----

[](#cache)

`InteractsWithCache` swaps every cache pool for an `ArrayAdapter`:

```
use Tyloo\Atc\Trait\InteractsWithCache;

final class RateLimitTest extends ApiTestCase
{
    use InteractsWithCache;

    #[\PHPUnit\Framework\Attributes\Test]
    public function fourth_request_in_a_minute_is_throttled(): void
    {
        for ($i = 0; $i < 3; $i++) {
            $this->get('/api/search?q=foo')->assertStatusOk();
        }

        $this->get('/api/search?q=foo')->assertStatus(429);

        $this->clearCache(); // reset between sub-scenarios
        $this->get('/api/search?q=foo')->assertStatusOk();
    }
}
```

Profiler &amp; N+1 detection
----------------------------

[](#profiler--n1-detection)

`InteractsWithProfiler` enables Symfony's Profiler per-request and exposes the captured `Profile`. Useful for catching N+1 query regressions:

```
use Tyloo\Atc\Trait\InteractsWithProfiler;

final class ListUsersTest extends ApiTestCase
{
    use InteractsWithProfiler;

    #[\PHPUnit\Framework\Attributes\Test]
    public function list_endpoint_uses_a_single_query_regardless_of_user_count(): void
    {
        UserFactory::createMany(50);

        $this->withProfiling();

        $this->get('/api/users')
            ->assertStatusOk()
            ->assertJsonCount(50, 'data');

        $this->assertQueryCount(1);            // exactly one SELECT
        $this->assertQueryCountLessThan(3);    // looser bound

        $profile = $this->profile();           // raw Symfony Profile for deeper introspection
    }
}
```

Requires `framework.profiler` enabled in the test kernel and `doctrine/doctrine-bundle` (for the `db` collector).

Customization
-------------

[](#customization)

There is no bundle, no YAML config, no DI extension. Every default lives on a protected method that you override in a base test case. Define one base class for your project and inherit everywhere:

```
use Tyloo\Atc\ApiTestCase;
use Tyloo\Atc\Http\ApiClient;

abstract class BaseApiTestCase extends ApiTestCase
{
    /** Default headers sent with every request. */
    #[\Override]
    protected function resolveDefaultHeaders(): array
    {
        return ['Accept' => 'application/json', 'X-Tenant' => 'acme'];
    }

    /** Where JSON Schema files live (`/tests/Schemas` by default). */
    #[\Override]
    protected function resolveJsonSchemaBaseDir(): string
    {
        return static::$kernel->getProjectDir() . '/tests/api-schemas';
    }

    /** Services replaced with default doubles for every test in this suite. */
    #[\Override]
    protected function defaultMocks(): array
    {
        return [
            ShopifyService::class => fn () => $this->createMock(ShopifyService::class),
        ];
    }

    /** Auth strategy: map a user to an authenticated client. */
    #[\Override]
    protected function authenticate(object $user, ApiClient $client): ApiClient
    {
        return $client->withToken($this->jwt()->create($user));
    }

    /** Pin the in-memory messenger transports instead of auto-discovering. */
    #[\Override]
    protected function resolveMessengerTransports(): array
    {
        return ['async', 'failed'];
    }

    /** Let unmatched outbound HTTP requests pass through instead of failing. */
    #[\Override]
    protected function resolveHttpClientStrict(): bool
    {
        return false;
    }

    /** Swap additional cache pools (default: just `cache.app`). */
    #[\Override]
    protected function cachePoolIds(): array
    {
        return ['cache.app', 'cache.system'];
    }
}
```

Cherry-pick the overrides you need. The defaults handle the common case.

Compatibility
-------------

[](#compatibility)

RequirementVersionsPHP8.3 / 8.4 / 8.5Symfony6.4 (LTS) / 7.x / 8.xPHPUnit12 / 13Doctrine ORM (optional)^3.6DoctrineBundle (optional)^2.18 || ^3.2CI runs the full matrix on each push.

Recommended companions
----------------------

[](#recommended-companions)

- [Zenstruck Foundry](https://github.com/zenstruck/foundry): test data factories.
- [DAMA/DoctrineTestBundle](https://github.com/dmaicher/doctrine-test-bundle): transactional DB rollback per test.

Inspirations
------------

[](#inspirations)

ATC borrows ideas from:

- [Laravel's testing helpers](https://laravel.com/docs/testing): fluent `actingAs` / `assertOk` style and the "what should the test actually look like" north star.
- [`api-platform/core`'s `ApiTestCase`](https://api-platform.com/docs/distribution/testing/): convention of subclassing `WebTestCase` with HTTP-centric helpers.
- [`zenstruck/browser`](https://github.com/zenstruck/browser): `KernelBrowser`-based fluent testing, profiler integration, and in-memory infrastructure capture patterns.

Contributing
------------

[](#contributing)

See [CONTRIBUTING.md](CONTRIBUTING.md) and our [Code of Conduct](CODE_OF_CONDUCT.md).

Security
--------

[](#security)

Report vulnerabilities via [GitHub Security Advisories](https://github.com/tyloo/atc/security/advisories/new). See [SECURITY.md](SECURITY.md).

License
-------

[](#license)

[MIT](LICENSE) © Julien Bonvarlet

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance94

Actively maintained with recent releases

Popularity3

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity38

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% 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

Unknown

Total

1

Last Release

28d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/3f248e28bcab86bf78322c5dc9c7d96eeaf1e95fd607b158fa998ebfa5681495?d=identicon)[Tyloo](/maintainers/Tyloo)

---

Top Contributors

[![tyloo](https://avatars.githubusercontent.com/u/611035?v=4)](https://github.com/tyloo "tyloo (1 commits)")

---

Tags

testingphpunitapisymfonyjson-schema

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/tyloo-atc/health.svg)

```
[![Health](https://phpackages.com/badges/tyloo-atc/health.svg)](https://phpackages.com/packages/tyloo-atc)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.5M370](/packages/easycorp-easyadmin-bundle)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.4M195](/packages/sulu-sulu)[shopsys/http-smoke-testing

HTTP smoke test case for testing all configured routes in your Symfony project

67266.6k2](/packages/shopsys-http-smoke-testing)[web-auth/webauthn-framework

FIDO2/Webauthn library for PHP and Symfony Bundle.

51090.8k2](/packages/web-auth-webauthn-framework)[web-auth/webauthn-symfony-bundle

FIDO2/Webauthn Security Bundle For Symfony

66474.5k8](/packages/web-auth-webauthn-symfony-bundle)[2lenet/crudit-bundle

The easy like Crud'it Bundle.

1715.6k12](/packages/2lenet-crudit-bundle)

PHPackages © 2026

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