The Ultimate Guide to Pionia Generic Services.
Tags: pionia, php, webdev, api
Published: 2024-07-06 06:56:04

[Pionia framework](https://pionia.netlify.app/) is a PHP Rest framework that is changing how we used to develop Rest platforms. Unlike all the existing frameworks, it gives the entire process a whole new look, making the development of APIs much simpler and less boring. This is because it runs on a different and rather 'new' pattern called `moonlight`. 

Per se, moonlight is not a new architecture/pattern, most institutions/companies/developers have been using it but just unnamed. But today, we aren't talking about moonlight, you can [read about it in my other article here](https://dev.to/jet_ezra/moonlight-architecture-the-old-new-4ph5) and even leave your comments.
 
To bootstrap a new Pionia project, you need to run the following command assuming you already have composer set up.

Let's create a todo_app.

```bash
composer create-project pionia/pionia-app todo_app
```

You can also run the same project using the pionia command like below:
```bash
php pionia serve
```

To watch you logs as they happen in real time, open a second terminal and run the following command:
```bash 
tail -f server.log
```

## Background on services.
Services in the Pionia framework are the heart, probably the only part you will spend most of your time while developing your APIs. All normal services in Pionia extend `Pionia\Request\BaseRestService`. A normal service in Pionia may look like this.

```php
namespace application\services;


use Exception;
use Pionia\Request\BaseRestService;
use Pionia\Response\BaseResponse;
use Porm\Porm;

class UserService extends BaseRestService
{
    /**
     * @throws Exception
     */
    protected function login(array $data): BaseResponse
    {
        // password and username are required, without them we won't proceed even
        $this->requires(["username", "password"]);

        $username = $data["username"];
        $password = password_hash($data['password'], PASSWORD_DEFAULT);

        $user = Porm::from('user')->get(['username' => $username, 'password' => $password]);
        //You can do more here, maybe generate a JWT token or add more checks
        // for example if the user is an active or not
        if ($user) {
            return BaseResponse::JsonResponse(0, "Login successful", $user);
        }
        throw new Exception("User with the given username and password not found");
    }

}
```
After building your service, you need to register it in the switch that shall be handling it from now on. If you are not aware of switches in Pionia, you can [read about them here in the docs](https://pionia.netlify.app/documentation/pionia-services/#service-registration). So, head over to our `switches` folder, probably in the `MainAppSwitch.php` if you have not yet created another one, and register the above service as below in the `registerServices` method

```php
     /**
     * Register your services here.
     *
     * @return array
     */
    public function registerServices(): array
    {
        return [
            'user' => new UserService(),
            'todo' => new TodoService()
        ];
    }
```

> This is how you get your service to be auto-discovered by the kernel from now on. In typical settings, you would have added a router, and a controller to map to this service, but Pionia approaches things differently. Remember, you can have the same service registered in multiple switches. This is how we achieve the concept of API versioning since every switch is handled by its API endpoint. By default, `MainAppSwitch` can be accessed on `/api/v1/`.

In your requests, you can point to this service by sending the following.

```json
// POST http://localhost:8000/api/v1/
{
    "SERVICE": "user",
    "ACTION": "login",
    "username": "pionia",
    "password": "pionia1234"
}
```
If you notice, `ACTION` is the name on the `action`/`method` we created in our `SERVICE`/`service`/`class` which we baptised the name `user` upon registration.

This is how normal services work in Pionia. 

Below is a complete service that performs CRUD in Piona. It is based on the following simple table called todos in a MySQL database called `todo_db`.
```SQL
create table todo_db.todos
(
    id          int auto_increment primary key,
    title       varchar(200)                        not null,
    description text                                null,
    created_at  timestamp default CURRENT_TIMESTAMP null
) engine = InnoDB;
```

```php

use Exception;
use Pionia\Request\BaseRestService;
use Pionia\Request\PaginationCore;
use Pionia\Response\BaseResponse;
use Porm\exceptions\BaseDatabaseException;
use Porm\Porm;

class TodoService extends BaseRestService
{
    /**
     * Returns all todos
     * @throws Exception
     */
    public function list(): BaseResponse
    {
        $result = Porm::table('todos')
            ->using('db')
            ->columns(['id', 'title', 'description', 'created_at'])
            ->all();

        return BaseResponse::JsonResponse(0, null, $result);
    }

    /**
     * Returns a single todo
     * @throws Exception
     */
    public function details(array $data): BaseResponse
    {
        $this->requires(['id']);
        $id = $data['id'];

        $result = Porm::table('todos')
            ->using('db')
            ->columns(['id', 'title', 'description', 'created_at'])
            ->get(['id' => $id]);

        return BaseResponse::JsonResponse(0, null, $result);
    }

    /**
     * Creates a new todo
     * @throws Exception
     */
    public function create(array $data): BaseResponse
    {
        $this->requires(['title', 'description']);
        $title = $data['title'];
        $description = $data['description'];

        $result = Porm::table('todos')
            ->save(['title' => $title, 'description' => $description]);

        return BaseResponse::JsonResponse(0, 'Todo created successfully', $result);
    }

    /**
     * Updates a todo
     * @throws Exception
     */
    public function update(array $data): BaseResponse
    {
        $this->requires(['id']);

        $id = $data['id'];

        $todo = Porm::table('todos')
            ->get($id); // similar to `get(['id' => $id])`

        // if the todo is not found, we throw an exception
        if (!$todo) {
            throw new BaseDatabaseException('Todo not found');
        }

        $description = $data['description'] ?? $todo->description;
        $title = $data['title'] ?? $todo->title;

        // we update in a transaction as below
        $result= null;
        Porm::table('todos')
            ->inTransaction(function () use ($description, $title, $id, &$result) {
                Porm::table('todos')
                    ->update(['description' => $description, 'title' => $title], $id);

                $result = Porm::table('todos')
                    ->get($id);
            });

        return BaseResponse::JsonResponse(0, "Todo $id updated successfully", $result);
    }

    /**
     * Deletes a todo
     * @throws Exception
     */
    public function delete(array $data): BaseResponse
    {
        $this->requires(['id']);
        $id = $data['id'];

        $todo = Porm::table('todos')
            ->get($id);

        if (!$todo) {
            throw new BaseDatabaseException('Todo not found');
        }

        $deleted = false;
        Porm::table('todos')
            ->inTransaction(function () use ($id, &$deleted) {
                Porm::table('todos')
                    ->delete($id);
                $deleted = true;
            });
        if (!$deleted) {
            throw new BaseDatabaseException('Todo not deleted');
        }
        return BaseResponse::JsonResponse(0, "Todo $id deleted successfully");
    }

    /**
     * Returns a random todo object if the size is not defined or 1,
     * else returns an array of random todos
     * @throws Exception
     */
    public function random($data): BaseResponse
    {
        $size = $data['size'] ?? 1;

        $result = Porm::table('todos')
            ->random($size);

        return BaseResponse::JsonResponse(0, null, $result);
    }

    /**
     * Returns a paginated list of todos
     * @throws Exception
     */
    public function paginatedList(array $data): BaseResponse
    {
        $offset = $data['offset'] ?? 0;
        $limit = $data['limit'] ?? 3;

        $paginator = new PaginationCore($data, 'todos', $limit, $offset, 'db');
        $result = $paginator->paginate();

        return BaseResponse::JsonResponse(0, null, $result);
    }
}
```

And since our `TodoService` is already registered, that's all we need to do, no adding extra routes, no adding controllers, just start hitting the `actions` in your requests and you should get uniform responses from all the above actions.

> **However much this is not alot to do, and the only thing to do in Pionia(building services), all the above actions in our `TodoService` can be omitted and we still get the same functionality, This is where our `Generic Services` come in!**

## Todo Service, The Generic Way.
If your logic is not more than `create`, `delete`, `paginate`, `list`, `update`, `delete`, or `retrieve` then Generic Services might be all you need. 

Pionia provides both generic services and mixins to use. Mixins can be put together to put up your entire new Generic Service. 

Provided mixins include `ListMixin`, `CreateMixin`, `DeleteMixin`, `UpdateMixin`, `RandomMixin` and `RetrieveMixin`. Under the hood, even Generic services are just combining these Mixins while extending `GenericService`.

Provided Generic Services include `RetrieveCreateUpdateService`, `RetrieveListCreateService`, `RetrieveListCreateUpdateDeleteService`, `RetrieveListDeleteService`, `RetrieveListRandomService`, `RetrieveListUpdateDeleteService`, `RetrieveListUpdateService` and `UniversalGenericService`.

If the above Generics don't combine the mixins in the way you want, you can extend `GenericService` and call all the mixins you want to use thus creating your custom generic service. 

> Remember, to use mixins, you must be extending `Pionia\Generics\Base\GenericService` not the normal `BaseRestService` we extended before. Also, remember that mixins are just PHP traits and should be used that way.

To refactor our `TodoService`, we shall need the last mentioned generic service, `UniversalGenericService` as it uses all the defined mixins.

Let's start by changing the class we extend. Refactor this as below
```php
use Pionia\Generics\UniversalGenericService;
// ... rest of the imports

class TodoService extends UniversalGenericService
{
// ... rest of your actions
}
```
Before we do anything, let's first define the table we want to target in the database. We use the $table property for this. This is a compulsory feature and must be defined for all generic views.
```php
use Pionia\Generics\UniversalGenericService;
// ... rest of the imports

class TodoService extends UniversalGenericService
{
   public string $table = "todo";
// ... rest of your actions
}
```

Secondly, from our `list` action, we are defining columns we want to return, however, we are defining all. If you want to return a certain range of columns only, we define the `$listColumns`(_which defaults to all_) and pass the columns we want to return. Let's just still pass all though it is the default behavior of the service.
```php
use Pionia\Generics\UniversalGenericService;
// ... rest of the imports

class TodoService extends UniversalGenericService
{
    public string $table = "todo";
    public ?array $listColumns = ['id', 'title', 'description', 'created_at'];

// ... rest of your actions
}
```
At this point, we can delete the `list` action from our service. That's complete!

Our second target action is now `details`. This one can be replaced by defining the `$pk_field` which defaults to `id`. Since our primary key field for our `todo` table is also `id`, we don't need to define it, we just need to delete it too! Remember, this one also uses the defined `$listColumns` for columns to return from the DB.
The `RetrieveMixin` also defines another sister action to this called `retrieve`, so in your request, you can use `ACTION` as `details` or `retrieve`, the two will perform the same thing.
Since we already have all we need, we can drop the `details` action too!

Our third action is `create`. For this, we **must** define the `$createColumns` to define those columns we shall be looking for from the request(required) to create a record. Let's add the property now.
```php
use Pionia\Generics\UniversalGenericService;
// ... rest of the imports

class TodoService extends UniversalGenericService
{
    public string $table = "todo";
    public ?array $listColumns = ['id', 'title', 'description', 'created_at'];
    public ?array $createColumns = ['title', 'description'];

// ... rest of your actions
}
```
After adding, go ahead and delete it too!

Our fourth action is `update`. For this, we require the `$pk_field` and can also **optionally** define the `$updateColumns`. If undefined, the responsible mixin checks if any of the properties were defined in the request, and will update only those.
Let's add the `$updateColumns` and give it the only properties we intend to update.
 ```php
use Pionia\Generics\UniversalGenericService;
// ... rest of the imports

class TodoService extends UniversalGenericService
{
    public string $table = "todo";
    public ?array $listColumns = ['id', 'title', 'description', 'created_at'];
    public ?array $createColumns = ['title', 'description'];
    public ?array $updateColumns = ['title', 'description'];

// ... rest of your actions
}
```
We can now drop the `update` action too!

For our fifth action, `delete`, we only need the `$pk_field` which is by default `id`, so we shall be checking if `id` was passed in the request, and then we delete the associated record. So, just delete it, we already have all we need!

Now to our sixth action, `random`, this also uses the `$listColumns` to determine the columns to fetch from the DB per record. We already have out property defined, so, just drop it too!

For our seventh action, `paginatedList`, we can drop it, and in any request, we target our `list` action, but we define any of the following pairs of keys in our request. 
1. `limit` and `offset` on the request object level.
```json
{
   "SERVICE": "todo",
   "ACTION": "list",
   "limit": 3,
   "offset": 0
}
```
2. `PAGINATION` or `pagination` object on the request with `limit` and `offset` keys.
```json
{
   "SERVICE": "todo",
   "ACTION": "list",
   "PAGINATION": {
      "limit": 3,
      "offset": 0,
   }
}
```

3. `SEARCH` or `search` object on the request object with `limit` and `offset` keys.
```json
{
   "SERVICE": "todo",
   "ACTION": "list",
   "SEARCH": {
      "limit": 3,
      "offset": 0
   }
}
```

> Note: Both the `limit` and `offset` keys must be defined for pagination to kick in.

And just like that, our service now has been reduced to the following.
 ```php
use Pionia\Generics\UniversalGenericService;

class TodoService extends UniversalGenericService
{
    public string $table = "todo";
    public ?array $listColumns = ['id', 'title', 'description', 'created_at'];
    public ?array $createColumns = ['title', 'description'];
    public ?array $updateColumns = ['title', 'description'];
}
```
Let's do a little more cleanup. As we had mentioned earlier, if we are listing all columns from our table, then we don't need to define the `$listColumns` property, let's remove that too. 
 ```php
use Pionia\Generics\UniversalGenericService;

class TodoService extends UniversalGenericService
{
    public string $table = "todo";
    public ?array $createColumns = ['title', 'description'];
    public ?array $updateColumns = ['title', 'description'];
}
```
Also, since our `update` can also discover the columns to update from the request data, let's remove the `$updateColumns` too!

And we are left with the following as our new `TodoService` but still exposing the actions of `list`(all and paginated), `create`, `update`, `delete`, `retrieve` or `details` and `random`
```php
use Pionia\Generics\UniversalGenericService;

class TodoService extends UniversalGenericService
{
    public string $table = "todo";
    public ?array $createColumns = ['title', 'description'];
}
```
You can also override how we get a single record and multiple records. You might not need it, but sometimes you may need to add `where` clauses and other conditions as you see fit. For that, you can [read about it in this section in the docs](https://pionia.netlify.app/documentation/generic-services/#overriding-getters).

Also, you may want to add your other actions in the same generic service, this is fully supported and will work as if you're in normal services, however, make sure none of those actions share the names with the provided mixin actions or otherwise you stand a chance of overriding the provided actions.

This also drives to the last point, what if you intend to override the default action? that's also okay! You can also look into it under [this section of the docs](https://pionia.netlify.app/documentation/generic-services/#overriding-actions).

Welcome to Pionia Framework, where we believe in both developer and program performance, writing precise and maintainable codebase with simplicity both at the front end and the back end!

Let me hear what you say about the Pionia Framework specifically about generic services. Happy coding!