Getting started with Pionia Framework
Tags: webdev, pionia, restapi, php
Published: 2024-07-06 19:30:29

For this specific article, I will save you from the backgrounds and inspirations of the framework. You can always find those in the [official framework documentation here](https://pionia.netlify.app).

However, let's get to work and draft a simple API in less than 20 minutes.

The following sections assume you're running PHP 8.1+, the composer is already set up and you have any RDBMS database preferably one of MySQL and Postgres.

## What we shall be working on today.

We shall create an API that:-

1. Creates a todo
    
2. Deletes a todo
    
3. Marks a todo as complete
    
4. Queries completed todos
    
5. Queries 1 or more random incomplete todo\[s\]
    
6. Returns a list of all todos
    
7. Returns a list of paginated todos
    
8. Updates a todo
    
9. Returns a single todo items
    
10. Returns overdue todos
    

This sounds and looks simple, but by the end of the day, you shall be able to perform all CRUD operations in Pionia, data filtration and interacting with Pionia APIs.

Our project shall be called todoApp.

For starters, we shall need to bootstrap a Pionia project using the following command.

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

On successful installation, you should have a directory [similar to this](https://pionia.netlify.app/documentation/application-structure/) though missing a few folders. Our focus folder is **app/services.** Other folders can be added when needed especially using our `pionia` command.

Pionia does not use models, therefore, it can work with only existing databases. For starters, you need to create your database, whether in Postgres, SQLite, MySQL, or any other database supported by PHP PDO.

For this guide, we shall use MySQL and create a database called `todo_app_db` . In your `mysql` console run the following.

```sql
CREATE DATABASE todo_app_db;
use todo_app_db;
```

Then, let's add the table that we shall be working with.

```sql
create table todo
(
    id          bigint auto_increment,
    title       varchar(225)                        null,
    description text                                null,
    created_at  timestamp default CURRENT_TIMESTAMP null,
    start_date  date                                not null,
    end_date    date                                not null,
    completed   bool      default false             null,
    constraint table_name_pk
        primary key (id)
);
```

This is all outside the Pionia framework. Let's come back to Pionia now.

We already have our default switch targeting `/api/v1/` registered already. This can be viewed in the `routes.php` file.

```php
use Pionia\Core\Routing\PioniaRouter;

$router = new PioniaRouter();

$router->addSwitchFor("application\switches\MainApiSwitch");

return $router->getRoutes();
```

And if we look at `application\switches\MainApiSwitch`, we find it is registering the `UserService`. Let's drop that and create our service. Remember to remove the import too - `use application\services\UserService;`

```php
public function registerServices(): array
    {
        return [
            'user' => new UserService(), // remove this
        ];
    }
}
```

Now it should be looking like this.

```php
public function registerServices(): array
    {
        return [
            
        ];
    }
}
```

Head over to `services` folder and remove the `UserService` too. We want to add our own.

In your terminal, run the following command.

```bash
 php pionia addservice todo
```

This shall create our new service `TodoService` in the services folder looking like this.

```php
<?php

/**
 * This service is auto-generated from pionia cli.
 * Remember to register your this service as TodoService in your service switch.
 */

namespace application\services;

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

class TodoService extends BaseRestService
{
	/**
	 * In the request object, you can hit this service using - {'ACTION': 'getTodo', 'SERVICE':'TodoService' ...otherData}
	 */
	protected function getTodo(?array $data, ?array $files): BaseResponse
	{
		return BaseResponse::JsonResponse(0, 'You have reached get action');
	}


	/**
	 * In the request object, you can hit this service using - {'ACTION': 'createTodo', 'SERVICE':'TodoService' ...otherData}
	 */
	protected function createTodo(?array $data, ?array $files): BaseResponse
	{
		return BaseResponse::JsonResponse(0, 'You have reached create action');
	}


	/**
	 * In the request object, you can hit this service using - {'ACTION': 'listTodo', 'SERVICE':'TodoService' ...otherData}
	 */
	protected function listTodo(?array $data, ?array $files): BaseResponse
	{
		return BaseResponse::JsonResponse(0, 'You have reached list action');
	}


	/**
	 * In the request object, you can hit this service using - {'ACTION': 'deleteTodo', 'SERVICE':'TodoService' ...otherData}
	 */
	protected function deleteTodo(?array $data, ?array $files): BaseResponse
	{
		return BaseResponse::JsonResponse(0, 'You have reached delete action');
	}
}
```

Before doing anything else, let's first register our service in the `MainApiSwitch` in our services under `registerServices` like this.

Also, since we don't intend to upload anything, let's remove all `?array $files` from our actions.

```php
public function registerServices(): array
    {
        return [
            'todo' => new TodoService(),
        ];
    }
```

So, let's test what we have so far. Run the server using the following command.

```php
php pionia serve
```

Your app is being served under `http://localhost:8000` but your API is served under `/api/v1/` . Let's first open `http://localhost:8000`, you should see the following.

![New Pionia API landing page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ykw9basdsq9szc2ryo4n.png)

Now let's try to open `http://localhost:8000/api/v1` in browser. If you have JSONViewer installed, you should see this.


![Pionia API status check via get request](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dfr9t7uzb60zd4gwokaj.png)



All `get` requests in Pionia will return the above! Therefore to test our service we need to make POST requests, we might not pull that off in the browser, so, I suggest we use Postman.

Fire up your postman or whatever you want to use. I will use Postman. All our services can receive either JSON or form data. Form data should be preferred for file uploads.

For now, let's send JSON data.

```json
{
    "SERVICE": "todo",
    "ACTION": "getTodo"
}
```

And we should get back the following.

```json
{
    "returnCode": 0,
    "returnMessage": "You have reached get action",
    "returnData": null,
    "extraData": null
}
```

This is what we are returning in our `getTodo`:-

```php
protected function getTodo(?array $data): BaseResponse
{
    return BaseResponse::JsonResponse(0, 'You have reached get action');
}
```

More about how we discovered the right action can be found [here in the official docs](https://pionia.netlify.app/documentation/services/)

Now, let's add our database settings in our `settings.ini` file like this.

```ini
[db]
;change this to your db name
database = "todo_app_db"
;change this to your db username
username = "root"
type = "mysql"
host = "localhost"
password = ""
port = 3306
```

To see what is happening in real time, Pionia ships in with a logger, open a new terminal window and run the following command.

```ini
tail -f server.log
```

You can also edit the logging behaviour in the settings file as below.

```ini
[SERVER]
port=8000
DEBUG=true
LOG_REQUESTS=true
PORT=8000
LOG_DESTINATION=server.log ; the file to log to.
NOT_FOUND_CODE=404
UNAUTHENTICATED_CODE=401
SERVER_ERROR_CODE=500
HIDE_IN_LOGS= ; what fields should be encrypted in logs
HIDE_SUB= ; the string to replace with the hidde value default is ********
LOGGED_SETTINGS= ; the settings to add in logs eg db,SERVER
LOG_FORMAT=TEXT ; can be json or test
;APP_NAME= ; you can override this as the app name. default is Pionia.
```

After setting this up, you be able to now view all requests and responses in your terminal.

So far we have not yet started coding anything. Just adding optional configurations to our app.

Let's start by creating a todo in our `createTodo` action.

```php
protected function createTodo(?array $data): BaseResponse
	{
        $this->requires(['title', 'description', 'start_date', 'end_date']);

        $title = $data['title'];
        $description = $data['description'];
        $startDate = date( 'Y-m-d', strtotime($data['start_date']));
        $endDate = date( 'Y-m-d', strtotime($data['end_date']));


        $saved = Porm::table('todo')->save([
            'title' => $title,
            'description' => $description,
            'start_date' => $startDate,
            'end_date' => $endDate
        ]);

		return BaseResponse::JsonResponse(0,
            'You have successfully created a new todo',
            $saved
        );
	}
```

Then let's send our request to target this action with all the required data like this.

```json
{
    "SERVICE": "todo",
    "ACTION": "createTodo",
    "title": "Am a brand new todo",
    "description":"Am the description of the brand new todo",
    "start_date":"12/12/2024",
    "end_date":"06/06/2024"
}
```

And we shall get back our response like this.

```json
{
    "returnCode": 0,
    "returnMessage": "You have successfully created a new todo",
    "returnData": {
        "id": 12,
        "title": "Am a brand new todo",
        "description": "Am the description of the brand new todo",
        "created_at": "2024-07-06 22:23:47",
        "start_date": "2024-12-12",
        "end_date": "2024-06-06",
        "completed": 0
    },
    "extraData": null
}
```

To shed some light on what is going on. The client requested `/api/v1/`, in the request, the client defined the `SERVICE` it is targeting, and the `ACTION`. This came straight to our `index.php` which also calls the kernel and sends all these requests to it.

The kernel sanitises the request and checks if there is an endpoint that handles `/api/v1`, it then sends the entire sanitised request to the switch in our case, the `MainApiSwich`, this in turn checks if it has any `service` that matches the `SERVICE` name that came through our request. It discovered that there is and it is called `TodoService`. It loads this service and calls the method that matches the name of the `ACTION` key, passing along all the request data as `$data`.

The action also expects certain data to be available in the request. This can be observed on this line.

```php
$this->requires(['title', 'description', 'start_date', 'end_date']);
```

If any of the required data is not found on the request. The request will abort with a clean exception. Let's test this.

In your postman, delete the `title` key and send it as follows:-

```json
{
    "SERVICE": "todo",
    "ACTION": "createTodo",
    "description":"Am the description of the brand new todo",
    "start_date":"12/12/2024",
    "end_date":"06/06/2024"
}
```

This will fail like below:-

```json
{
    "returnCode": 500,
    "returnMessage": "The field title is required",
    "returnData": null,
    "extraData": null
}
```

Notice that all scenarios still return an HTTP Status Code of 200 OK, but different returnCode\[s\]. Also, notice that the response format stays the same throughout. This is [moonlight pattern](https://pionia.netlify.app/moonlight/introduction-to-moonlight-architecture/) in action!

This is how you can pull off actions in Pionia. However, Pionia suggests that if what you are looking for is just CRUD, then you can look into [Generic Services](https://pionia.netlify.app/documentation/generic-services/). If you do not know about generic services, you can read about these in [one of my articles here](https://hashnode.com/post/cly9sd3zk000f09l6cy3qdjbj).

So, proceeding, let's first create the entire CRUD and see what generic services can help us reduce.

Full-Service code so far for our `TodoService`

```php
<?php

/**
 * This service is auto-generated from pionia cli.
 * Remember to register this service as TodoService in your service switch.
 */

namespace application\services;

use Exception;
use Pionia\Exceptions\FailedRequiredException;
use Pionia\Request\BaseRestService;
use Pionia\Request\PaginationCore;
use Pionia\Response\BaseResponse;
use Porm\database\aggregation\Agg;
use Porm\database\builders\Where;
use Porm\exceptions\BaseDatabaseException;
use Porm\Porm;

class TodoService extends BaseRestService
{
    /**
     * In the request object, you can hit this service using - {'ACTION': 'getTodo', 'SERVICE':'TodoService' ...otherData}
     * @throws Exception
     */
	protected function getTodo(?array $data): BaseResponse
	{
		$this->requires(['id']);
        $id = $data['id'];
        $todo = Porm::table('todo')->get($id);
        return BaseResponse::JsonResponse(0, null, $todo);
	}


    /**
     * In the request object, you can hit this service using - {'ACTION': 'createTodo', 'SERVICE':'TodoService' ...otherData}
     * @throws Exception
     */
	protected function createTodo(?array $data): BaseResponse
	{
        $this->requires(['title', 'description', 'start_date', 'end_date']);

        $title = $data['title'];
        $description = $data['description'];
        $startDate = date( 'Y-m-d', strtotime($data['start_date']));
        $endDate = date( 'Y-m-d', strtotime($data['end_date']));


        $saved = Porm::table('todo')->save([
            'title' => $title,
            'description' => $description,
            'start_date' => $startDate,
            'end_date' => $endDate
        ]);

		return BaseResponse::JsonResponse(0,
            'You have successfully created a new todo',
            $saved
        );
	}


    /**
     * In the request object, you can hit this service using - {'ACTION': 'listTodo', 'SERVICE':'TodoService' ...otherData}
     * @throws BaseDatabaseException
     */
	protected function listTodo(?array $data): BaseResponse
	{
        $todos = Porm::table('todo')->all();
		return BaseResponse::JsonResponse(0, null, $todos);
	}


    /**
     * In the request object, you can hit this service using - {'ACTION': 'deleteTodo', 'SERVICE':'TodoService' ...otherData}
     * @throws Exception
     */
	protected function deleteTodo(?array $data): BaseResponse
	{
        $this->requires(['id']);
        $id = $data['id'];
        Porm::table('todo')->delete($id);
		return BaseResponse::JsonResponse(0, 'To-do deleted successfully');
	}

    /**
     * In the request object, you can hit this service using - {'ACTION': 'deleteTodo', 'SERVICE':'TodoService' ...otherData}
     * @throws Exception
     */
    protected function updateTodo(?array $data): BaseResponse
    {
        $this->requires(['id']);
        $id = $data['id'];
        $todo = Porm::table('todo')->get($id);
        if (!$todo) {
            throw new Exception("Todo with id $id not found");
        }

        $title = $data['title'] ?? $todo->title;
        $description = $data['description'] ?? $todo->description;
        $startDate = isset($data['start_date']) ? date( 'Y-m-d', strtotime($data['start_date'])) : $todo->start_date;
        $endDate = isset($data['end_date']) ? date( 'Y-m-d', strtotime($data['end_date'])) : $todo->end_date;
        $completed = $data['completed'] ?? $todo->completed;

        Porm::table('todo')->update([
            'title' => $title,
            'description' => $description,
            'start_date' => $startDate,
            'end_date' => $endDate,
            'completed' => $completed
        ], $id);

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

        return BaseResponse::JsonResponse(0, 'To-do updated successfully', $newTodo);
    }

    /**
     * @param $data
     * @return BaseResponse
     * @throws BaseDatabaseException
     * @throws Exception
     */
    protected function randomTodo($data): BaseResponse
    {
        $size = $data['size'] ?? 1;
        $todos = Porm::table('todo')->random($size);
        return BaseResponse::JsonResponse(0, null, $todos);
    }

    /**
     * @param $data
     * @return BaseResponse
     * @throws BaseDatabaseException
     * @throws Exception
     */
    protected function markComplete($data): BaseResponse
    {
        $this->requires(['id']);
        $id = $data['id'];

        $todo = Porm::table('todo')->get($id);
        if (!$todo) {
            throw new Exception("Todo with id $id not found");
        }
        if ($todo->completed) {
            throw new Exception("Todo with id $id is already completed");
        }

        Porm::table('todo')->update(['completed' => 1], $id);

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

        return BaseResponse::JsonResponse(0, 'To-do marked as completed', $newTodo);
    }

    /**
     * @throws BaseDatabaseException
     */
    protected function listCompletedTodos(): BaseResponse
    {
        $todos = Porm::table('todo')->where(['completed' => true])->all();
        return BaseResponse::JsonResponse(0, null, $todos);
    }

    /**
     * @throws BaseDatabaseException
     */
    protected function listPaginatedTodos($data): BaseResponse
    {
        $limit = $data['limit'] ?? 5;
        $offset = $data['offset'] ?? 0;

        $paginator = new PaginationCore($data, 'todo', $limit, $offset);

        $todos = $paginator->paginate();
        return BaseResponse::JsonResponse(0, null, $todos);
    }

    /**
     * @throws BaseDatabaseException
     */
    protected function listOverdueTodos(): BaseResponse
    {
        $today = date('Y-m-d');

        $todos = Porm::table('todo')
            ->where(
                Where::builder()->and([
                "end_date[<]" => $today,
                'completed' => false
            ])->build())
            ->all();

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

```

With the above, we have completed our entire checklist of all we needed to cover. Play with it in Postman to see if everything is functioning properly.

As you have noticed it, we only focused on services, nothing like controllers, routes, models!! This is how Pionia is changing how we develop APIs.

Let me know what you say about this framework in the comment section below.

Happy coding!