PHPackages                             rougin/dexter - 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. [Framework](/categories/framework)
4. /
5. rougin/dexter

ActiveLibrary[Framework](/categories/framework)

rougin/dexter
=============

"Ready-to-eat" CRUD for PHP backend.

v0.2.0(1mo ago)17591MITPHPPHP &gt;=5.3.0CI passing

Since May 23Pushed 3w ago1 watchersCompare

[ Source](https://github.com/rougin/dexter)[ Packagist](https://packagist.org/packages/rougin/dexter)[ Docs](https://roug.in/dexter/)[ RSS](/packages/rougin-dexter/feed)WikiDiscussions master Synced 1mo ago

READMEChangelog (3)Dependencies (10)Versions (3)Used By (0)

Dexter
======

[](#dexter)

[![Latest Version on Packagist](https://camo.githubusercontent.com/61670c7bea459fc7060355f0826c8e6bf82b23dd28f0507bb53475da5eedc64a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f726f7567696e2f6465787465722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/rougin/dexter)[![Software License](https://camo.githubusercontent.com/55c0218c8f8009f06ad4ddae837ddd05301481fcf0dff8e0ed9dadda8780713e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d627269676874677265656e2e7376673f7374796c653d666c61742d737175617265)](https://github.com/rougin/dexter/blob/master/LICENSE.md)[![Build Status](https://camo.githubusercontent.com/3e78839dac4fe466478d3d7ffc8d618fdb6370ffa2b8fc2d3ec4f1ee30ff2f1b/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f726f7567696e2f6465787465722f6275696c642e796d6c3f7374796c653d666c61742d737175617265)](https://github.com/rougin/dexter/actions)[![Coverage Status](https://camo.githubusercontent.com/3fa7bb132446f108ec007b20d9ff3df155b5116b5fcf6784302031330c16f655/68747470733a2f2f696d672e736869656c64732e696f2f636f6465636f762f632f6769746875622f726f7567696e2f6465787465723f7374796c653d666c61742d737175617265)](https://app.codecov.io/gh/rougin/dexter)[![Total Downloads](https://camo.githubusercontent.com/70f73c6a6d0d558bd669668dea6005a4135329db645738f0bd200437c3d5827d/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f726f7567696e2f6465787465722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/rougin/dexter)

`Dexter` is a utility PHP package that provides extensible PHP classes for handling [CRUD operations](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete). It can also create HTTP routes that conforms to the [PSR-07](https://www.php-fig.org/psr/psr-7/) standard.

```
namespace Rougin\Torin\Depots;

use Rougin\Dexter\Depots\EloquentDepot;
use Rougin\Torin\Models\Client;

class ClientDepot extends EloquentDepot
{
    protected $model;

    public function __construct(Client $client)
    {
        $this->model = $client;
    }
}
```

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

[](#installation)

Install the `Dexter` package via [Composer](https://getcomposer.org/):

```
$ composer require rougin/dexter
```

Using `Depot`
-------------

[](#using-depot)

The `Depot` class is a special PHP class which provides methods related to CRUD operations (e.g., `create`, `delete`, `find`, `update`):

```
namespace Acme\Depots;

use Rougin\Dexter\Depot;

class UserDepot extends Depot
{
    // ...
}
```

Using the `Depot` class improves development productivity as it reduces writing of code relating to CRUD operations. As it is also designed to be extensible, it can be used freely without the required methods.

Note

In other PHP frameworks and other guides, `Depot` is also known as `Repository` from the [Repository pattern](https://designpatternsphp.readthedocs.io/en/latest/More/Repository/README.html).

If a `Depot` class is used, the following methods must be defined depending on its usage:

### `create` method

[](#create-method)

The `create` method will be used for creating an item based on the provided payload:

```
// index.php

use Acme\Depots\UserDepot;

$depot = new UserDepot;

/** @var array */
$data = /** ... */;

/** @var \Acme\Sample\User */
$item = $depot->create($data);
```

If the specified method is being called, its logic must be defined from the `Depot` class:

```
namespace Acme\Depots;

use Acme\Sample\UserFactory;
use Rougin\Dexter\Depot;

class UserDepot extends Depot
{
    /**
     * Creates a new item.
     *
     * @param array $data
     *
     * @return \Acme\Sample\User
     */
    public function create($data)
    {
        return UserFactory::create($data);
    }

    // ...
}
```

If the required logic for the `create` method is not defined, it will throw a `LogicError`.

### `delete` method

[](#delete-method)

When deleting specified items, the `delete` method can be used from the `Depot` class:

```
// index.php

use Acme\Depots\UserDepot;

$depot = new UserDepot;

$depot->delete(99);
```

Using the `delete` method also requires other methods `deleteRow` and `rowExists` to be defined:

```
namespace Acme\Depots;

use Acme\Sample\UserDeleter;
use Acme\Sample\UserReader;
use Rougin\Dexter\Depot;

class UserDepot extends Depot
{
    // ...

    /**
     * Checks if the specified item exists.
     *
     * @param integer $id
     *
     * @return boolean
     */
    public function rowExists($id)
    {
        return UserReader::exists($id);
    }

    /**
     * Deletes the specified item.
     *
     * @param integer $id
     *
     * @return boolean
     */
    protected function deleteRow($id)
    {
        return UserDeleter::delete($id);
    }
}
```

If the required logic for the `delete` method is not defined, a `LogicError` will be thrown.

### `find` method

[](#find-method)

The `find` method is one of the CRUD operations that tries to find an item based on the given unique identifier (e.g., `id`):

```
// index.php

use Acme\Depots\UserDepot;

$depot = new UserDepot;

/** @var \Acme\Sample\User */
$item = $depot->find(99);
```

To use the `find` method, kindly write its logic in the `findRow` method:

```
namespace Acme\Depots;

use Acme\Sample\UserReader;
use Rougin\Dexter\Depot;

class UserDepot extends Depot
{
    // ...

    /**
     * Returns the specified item.
     *
     * @param integer $id
     *
     * @return \Acme\Sample\User
     * @throws \UnexpectedValueException
     */
    protected function findRow($id)
    {
        $item = UserReader::find($id);

        if (! $item)
        {
            throw new \UnexpectedValueException('Item not found');
        }

        return $item;
    }
}
```

If the specified identifier does not exists, it must throw an `UnexpectedValueException`. Likewise, if the required logic for the `find` method is not defined, it will throw a `LogicError`.

### `get` method

[](#get-method)

One of the methods of `Depot` that returns an array of items based on the specified page number and its rows to be shown per page:

```
// index.php

use Acme\Depots\UserDepot;

$depot = new UserDepot;

/** @var \Rougin\Dexter\Result */
$item = $depot->get(1, 10);
```

To use the `get` method, the methods `getItems` and `getTotal` must be defined:

```
namespace Acme\Depots;

use Acme\Sample\UserReader;
use Rougin\Dexter\Depot;

class UserDepot extends Depot
{
    // ...

    /**
     * Returns the total number of items.
     *
     * @return integer
     */
    public function getTotal()
    {
        return UserReader::totalRows();
    }

    /**
     * Returns the items with filters.
     *
     * @param integer $page
     * @param integer $limit
     *
     * @return \Acme\Sample\User[]
     */
    protected function getItems($page, $limit)
    {
        return UserReader::getByLimit($limit, $page);
    }
}
```

If the logic requires an offset instead of a page number, the `getOffset` method from `Depot` can be used to compute the said offset value:

```
namespace Acme\Depots;

use Acme\Sample\UserReader;
use Rougin\Dexter\Depot;

class UserDepot extends Depot
{
    // ...

    /**
     * Returns the items with filters.
     *
     * @param integer $page
     * @param integer $limit
     *
     * @return \Acme\Sample\User[]
     */
    protected function getItems($page, $limit)
    {
        $offset = $this->getOffset($page, $limit);

        return UserReader::items($offset, $limit);
    }

    // ...
}
```

Using the `get` method returns a `Result` class, which can be used for handling the result from the `Depot`:

```
// index.php

use Acme\Depots\UserDepot;

$depot = new UserDepot;

/** @var \Rougin\Dexter\Result */
$item = $depot->get(1, 10);

print_r($item->toArray());
```

Each item from the `Result` class can also be parsed manually using the `asRow` class:

```
namespace Acme\Depots;

use Acme\Sample\User;
use Acme\Sample\UserReader;
use Rougin\Dexter\Depot;

class UserDepot extends Depot
{
    // ...

    /**
     * Returns the parsed item.
     *
     * @param \Acme\Sample\User $row
     *
     * @return array
     */
    protected function asRow(User $row)
    {
        $data = array('id' => $row->id);

        $data['name'] = $row->name;

        $data['age'] = $row->age + 10;

        return $data;
    }

    // ...
}
```

If the required logic for the `get` method is not defined, a `LogicError` will be thrown.

### `update` method

[](#update-method)

The `update` method is used to update details of the specified item:

```
// index.php

use Acme\Depots\UserDepot;

$depot = new UserDepot;

/** @var array */
$data = /** ... */;

$depot->update(99, $data);
```

When using the `update` method, its required logic must also be defined:

```
namespace Acme\Depots;

use Acme\Sample\UserUpdater;
use Rougin\Dexter\Depot;

class UserDepot extends Depot
{
    // ...

    /**
     * Updates the specified item.
     *
     * @param integer              $id
     * @param array $data
     *
     * @return boolean
     */
    public function update($id, $data)
    {
        return UserUpdater::update($id, $data);
    }
}
```

If the logic for the `update` method is not defined, it will throw a `LogicError`.

Using `EloquentDepot`
---------------------

[](#using-eloquentdepot)

The `EloquentDepot` class provides a pre-built implementation of `Depot` that delegates all CRUD operations to an [Eloquent Model](https://laravel.com/docs/eloquent):

```
namespace Acme\Depots;

use Acme\Models\User;
use Illuminate\Database\Eloquent\Model;
use Rougin\Dexter\Depots\EloquentDepot;

class UserDepot extends EloquentDepot
{
    /**
     * @param \Acme\Models\User $user
     */
    public function __construct(User $user)
    {
        $this->model = $user;
    }
}
```

Once defined, all CRUD methods are available out of the box:

```
// index.php

use Acme\Depots\UserDepot;
use Acme\Models\User;

$depot = new UserDepot(new User);

// Create a new item ---------------
$data = array('name' => 'John Doe');

$data['email'] = 'john@example.com';

/** @var \Acme\Models\User */
$item = $depot->create($data);
// ---------------------------------

// Find an item by its ID ---
/** @var \Acme\Models\User */
$item = $depot->find(1);
// --------------------------

// Paginate through items -------
/** @var \Rougin\Dexter\Result */
$result = $depot->get(1, 10);

/** @var array */
$items = $result->toArray();
// ------------------------------

// Update an existing item ---------
$data = array('name' => 'Jane Doe');

$depot->update(1, $data);
// ---------------------------------
```

Note

The `EloquentDepot` class requires the `illuminate/database` package, which should be installed as a dependency:

```
$ composer require illuminate/database
```

Filtering results
-----------------

[](#filtering-results)

The `get` method supports filtering through the `withFilter` method. Use the `Filter` class to add `WHERE` clauses to the paginated query:

```
// index.php

use Acme\Depots\UserDepot;
use Acme\Models\User;
use Rougin\Dexter\Filter;

$depot = new UserDepot(new User);

// Filter by exact matches ---------
$filter = new Filter;

$filter->setInt('age', 25);

$filter->setStr('status', 'active');

$depot->withFilter($filter);
// ---------------------------------

/** @var \Rougin\Dexter\Result */
$result = $depot->get(1, 10);
```

The specified filters can be used as `as*` method in `getItems`:

```
namespace Acme\Depots;

use Rougin\Dexter\Depot;

class UserDepot extends Depot
{
    // ...

    /**
     * @param integer $page
     * @param integer $limit
     *
     * @return mixed[]
     */
    protected function getItems($page, $limit)
    {
        $status = $this->filter->asTrueStr('status');

        $model = $this->user;

        $model = $model->where('status', $status);

        return $model->get();
    }
}
```

To identify filter fields as searchable (using `LIKE`), use the `withSearch` method:

```
// index.php

use Acme\Depots\UserDepot;
use Acme\Models\User;
use Rougin\Dexter\Filter;

$depot = new UserDepot(new User);

// Search by name or email -----------------
$filter = new Filter;

$filter->setStr('name', 'John');

$filter->setStr('email', 'john@');

$filter->withSearch(array('name', 'email'));

$depot->withFilter($filter);
// -----------------------------------------

/** @var \Rougin\Dexter\Result */
$result = $depot->get(1, 10);
```

The searchable fields can be used in the depot with `getSearchKeys` method:

```
namespace Acme\Depots;

use Rougin\Dexter\Depot;

class UserDepot extends Depot
{
    // ...

    /**
     * @param integer $page
     * @param integer $limit
     *
     * @return mixed[]
     */
    protected function getItems($page, $limit)
    {
        $keys = $this->filter->getSearchKeys();

        $model = $this->user;

        foreach ($keys as $key)
        {
            $value = '%' . $this->filter->asTrueStr($key) . '%';

            $model = $model->where($key, 'like', $value);
        }

        return $model->get();
    }
}
```

Using `Route`
-------------

[](#using-route)

The `Route` class in `Dexter` is similar to the previously discussed `Depot` class. While the `Depot` class conforms to the [CRUD operations](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete), the `Route` class closely follows the [RESTful software architecture style](https://en.wikipedia.org/wiki/REST) and uses the [PSR-07](https://www.php-fig.org/psr/psr-7/) standard for standardization of its HTTP responses:

```
namespace Acme\Routes;

use Acme\Depots\UserDepot;
use Rougin\Dexter\Http\Response;
use Rougin\Dexter\Input;
use Rougin\Dexter\Route;

class Users extends Route
{
    protected $user;

    public function __construct(UserDepot $user)
    {
        $this->user = $user;
    }

    protected function setIndexData($params)
    {
        $query = new Input($params);

        $limit = $query->asTrueInt('l');

        $page = $query->asTrueInt('p');

        $result = $this->user->get($page, $limit);

        $result = $result->toArray();

        return Response::toJson($result);
    }
}
```

```
// index.php

use Acme\Depots\UserDepot;
use Acme\Routes\Users;

$depot = new UserDepot;

$route = new Users($depot);

/** @var \Psr\Http\Message\ResponseInterface */
$request = /** ... */

$response = $route->index($request);
```

The `Route` class contains the following methods for writing their logic:

**`is[METHOD]Valid`**

This method will be triggered if `[METHOD]` requires to be validated first. If not specified, it always return to `true` by default:

```
namespace Acme\Routes;

use Rougin\Dexter\Route;

class Users extends Route
{
    // ...

    /**
     * Checks if the items are allowed to be returned.
     *
     * @param array $params
     *
     * @return boolean
     */
    protected function isIndexValid($params)
    {
        return true;
    }
}
```

**`invalid[METHOD]`**

This method will be triggered if the `is[METHOD]Valid` method returns to `false`. This should return an HTTP response with an HTTP code between `4xx` to `5xx`:

```
namespace Acme\Routes;

use Rougin\Dexter\Http\Response;
use Rougin\Dexter\Route;

class Users extends Route
{
    // ...

    /**
     * Returns a response if the validation failed.
     *
     * @return \Psr\Http\Message\ResponseInterface
     */
    protected function invalidIndex()
    {
        return new Response(422);
    }
}
```

**`set[METHOD]Data`**

This is the main method that requires to write its logic based on `[METHOD]`:

```
namespace Acme\Routes;

use Rougin\Dexter\Http\Response;
use Rougin\Dexter\Route;

class Users extends Route
{
    // ...

    /**
     * Executes the logic for returning an array of items.
     *
     * @param array $params
     *
     * @return \Psr\Http\Message\ResponseInterface
     */
    protected function setIndexData($params)
    {
        $result = $this->user->get($params['page'], $params['limit']);

        return Response::toJson($result->toArray());
    }
}
```

Using this kind of approach improves the code structure of HTTP routes as it only requires to write the logic for each `Route` method that is being used (e.g., `index`).

Note

In other PHP frameworks and other guides, `Route` is also known as `Controller`.

### `delete` method

[](#delete-method-1)

The `delete` method is an HTTP route which can be used for deleting a specified item:

```
namespace Acme\Routes;

use Rougin\Dexter\Http\Response;
use Rougin\Dexter\Route;

class Users extends Route
{
    // ...

    /**
     * Returns a response if the validation failed.
     *
     * @return \Psr\Http\Message\ResponseInterface
     */
    protected function invalidDelete()
    {
        return new Response(404);
    }

    /**
     * Checks if the specified item can be deleted.
     *
     * @param integer $id
     *
     * @return boolean
     */
    protected function isRowValid($id)
    {
        return true;
    }

    /**
     * Executes the logic for deleting the specified item.
     *
     * @param integer $id
     *
     * @return \Psr\Http\Message\ResponseInterface
     */
    protected function setDeleteData($id)
    {
        $this->user->delete($id);

        return Response::toJson('Deleted!', 204);
    }
}
```

```
// index.php

// ...

/** @var \Acme\Depots\UserDepot */
$route = /** ... */;

$response = $route->delete($id);
```

### `index` method

[](#index-method)

The `index` method should return an array of items as its HTTP response:

```
namespace Acme\Routes;

use Rougin\Dexter\Http\Response;
use Rougin\Dexter\Route;

class Users extends Route
{
    // ...

    /**
     * Returns a response if the validation failed.
     *
     * @return \Psr\Http\Message\ResponseInterface
     */
    protected function invalidIndex()
    {
        return new Response(422);
    }

    /**
     * Checks if the items are allowed to be returned.
     *
     * @param array $params
     *
     * @return boolean
     */
    protected function isIndexValid($params)
    {
        return true;
    }

    /**
     * Executes the logic for returning an array of items.
     *
     * @param array $params
     *
     * @return \Psr\Http\Message\ResponseInterface
     */
    protected function setIndexData($params)
    {
        $result = $this->user->get($params['page'], $params['limit']);

        return Response::toJson($result->toArray());
    }
}
```

```
// index.php

// ...

/** @var \Psr\Http\Message\ResponseInterface */
$request = /** ... */

/** @var \Acme\Depots\UserDepot */
$route = /** ... */;

$response = $route->index($request);
```

### `show` method

[](#show-method)

The `show` method returns an HTTP response for the specified item:

```
namespace Acme\Routes;

use Rougin\Dexter\Http\Response;
use Rougin\Dexter\Route;

class Users extends Route
{
    // ...

    /**
     * Returns a response if the validation failed.
     *
     * @return \Psr\Http\Message\ResponseInterface
     */
    protected function invalidShow()
    {
        return new Response(422);
    }

    /**
     * Checks if the specified item is allowed to be returned.
     *
     * @param integer              $id
     * @param array $params
     *
     * @return boolean
     */
    protected function isRowValid($id)
    {
        return true;
    }

    /**
     * Executes the logic for returning the specified item.
     *
     * @param integer              $id
     * @param array $params
     *
     * @return \Psr\Http\Message\ResponseInterface
     */
    protected function setShowData($id, $params)
    {
        $item = $this->user->find($id);

        return Response::toJson($item);
    }
}
```

```
// index.php

// ...

/** @var \Psr\Http\Message\ResponseInterface */
$request = /** ... */

/** @var \Acme\Depots\UserDepot */
$route = /** ... */;

$response = $route->show(99, $request);
```

### `store` method

[](#store-method)

The `store` method should be responsible for creating new items to the specified storage:

```
namespace Acme\Routes;

use Rougin\Dexter\Http\Response;
use Rougin\Dexter\Route;

class Users extends Route
{
    // ...

    /**
     * Returns a response if the validation failed.
     *
     * @return \Psr\Http\Message\ResponseInterface
     */
    protected function invalidStore()
    {
        return new Response(422);
    }

    /**
     * Checks if it is allowed to create a new item.
     *
     * @param array $parsed
     *
     * @return boolean
     */
    protected function isStoreValid($parsed)
    {
        return true;
    }

    /**
     * Executes the logic for creating a new item.
     *
     * @param array $parsed
     *
     * @return \Psr\Http\Message\ResponseInterface
     */
    protected function setStoreData($parsed)
    {
        $this->user->create($parsed);

        return Response::toJson('Created!', 201);
    }
}
```

```
// index.php

// ...

/** @var \Psr\Http\Message\ResponseInterface */
$request = /** ... */

/** @var \Acme\Depots\UserDepot */
$route = /** ... */;

$response = $route->store($request);
```

### `update` method

[](#update-method-1)

The `update` method updates the details of a specified item:

```
namespace Acme\Routes;

use Rougin\Dexter\Http\Response;
use Rougin\Dexter\Route;

class Users extends Route
{
    // ...

    /**
     * Returns a response if the validation failed.
     *
     * @return \Psr\Http\Message\ResponseInterface
     */
    protected function invalidUpdate()
    {
        return new Response(422);
    }

    /**
     * Checks if the specified item can be updated.
     *
     * @param integer $id
     * @param array $parsed
     *
     * @return boolean
     */
    protected function isUpdateValid($id, $parsed)
    {
        return true;
    }

    /**
     * Executes the logic for updating the specified item.
     *
     * @param integer              $id
     * @param array $parsed
     *
     * @return \Psr\Http\Message\ResponseInterface
     */
    protected function setUpdateData($id, $parsed)
    {
        $this->user->update($id, $parsed);

        return Response::toJson('Updated!', 204);
    }
}
```

```
// index.php

// ...

/** @var \Psr\Http\Message\ResponseInterface */
$request = /** ... */

/** @var \Acme\Depots\UserDepot */
$route = /** ... */;

$response = $route->update(99, $request);
```

Unified validation
------------------

[](#unified-validation)

When multiple actions share the same validation logic, use the `invalid`, `isAllowed`, and `isDataValid` methods to avoid duplication.

### `invalid`

[](#invalid)

Returns an error response for any action. The `$code` parameter defaults to `400`:

```
namespace Acme\Routes;

use Rougin\Dexter\Http\Response;
use Rougin\Dexter\Route;

class Users extends Route
{
    // ...

    /**
     * @param integer $code
     *
     * @return \Psr\Http\Message\ResponseInterface
     */
    protected function invalid($code = 400)
    {
        $data = $this->check->errors();

        // HTTP 404 means the row does not exists ---
        if ($code === 404)
        {
            $data = 'User does not exists';
        }
        // ------------------------------------------

        return Response::toJson($data, $code);
}
```

All per-action methods (e.g., `invalidDelete`, `invalidStore`, etc.) are delegated to this method by default. Override the `invalid` method once when the error response is the same for all actions.

### `isAllowed`

[](#isallowed)

Checks whether the action is allowed (e.g., checking if the user is authorized):

```
namespace Acme\Routes;

use Rougin\Dexter\Route;

class Users extends Route
{
    // ...

    /**
     * @param array $data
     * @param integer              $id
     *
     * @return boolean
     */
    protected function isAllowed($data, $id = 0)
    {
        if ($id && ! $this->depot->rowExists($id))
        {
            return false;
        }

        return $this->auth->isAuthorized($data);
    }
}
```

All action methods delegated to this method by default. Override this method if all actions must have the same authorization logic.

### `isDataValid`

[](#isdatavalid)

Checks whether the payload data is valid for the `store` and `update` actions:

```
namespace Acme\Routes;

use Rougin\Dexter\Route;

class Users extends Route
{
    // ...

    /**
     * @param array $data
     * @param integer              $id
     *
     * @return boolean
     */
    protected function isDataValid($data, $id = 0)
    {
        return $this->check->valid($data);
    }
}
```

The `isStoreValid` and `isUpdateValid` methods are only delegated to methods `isAllowed` and `isDataValid`.

Changelog
---------

[](#changelog)

Please see [CHANGELOG](https://github.com/rougin/dexter/blob/master/CHANGELOG.md) for more recent changes.

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

[](#contributing)

See [CONTRIBUTING](https://github.com/rougin/dexter/blob/master/CONTRIBUTING.md) on how to contribute.

License
-------

[](#license)

The MIT License (MIT). Please see [LICENSE](https://github.com/rougin/dexter/blob/master/LICENSE.md) for more information.

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance94

Actively maintained with recent releases

Popularity22

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity19

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

Every ~7 days

Total

2

Last Release

32d ago

### Community

Maintainers

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

---

Top Contributors

[![rougin](https://avatars.githubusercontent.com/u/6078637?v=4)](https://github.com/rougin "rougin (148 commits)")

---

Tags

php-crudphp-depotphp-rapidphp-crudphp-depotphp-rapid

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/rougin-dexter/health.svg)

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

###  Alternatives

[symfony/symfony

The Symfony PHP framework

31.4k87.2M2.2k](/packages/symfony-symfony)[neuron-core/neuron-ai

The PHP Agentic Framework.

2.0k656.1k38](/packages/neuron-core-neuron-ai)[tempest/framework

The PHP framework that gets out of your way.

2.2k34.4k15](/packages/tempest-framework)[flow-php/flow

PHP ETL - Extract Transform Load - Data processing framework

85036.3k](/packages/flow-php-flow)[telnyx/telnyx-php

Official Telnyx PHP SDK — APIs for Voice, SMS, MMS, WhatsApp, Fax, SIP Trunking, Wireless IoT, Call Control, and more. Build global communications on Telnyx's private carrier-grade network.

35789.4k2](/packages/telnyx-telnyx-php)[fleetbase/core-api

Core Framework and Resources for Fleetbase API

1235.9k20](/packages/fleetbase-core-api)

PHPackages © 2026

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