Services

This section assumes that you have a basic understanding of the Pionia framework. If you are new to Pionia, you can start with the tutorial.

Introduction

Services in Pionia Framework are central holders of business logic. This is where most of the work happens. Pionia has tried to reduce your work from other areas so that you mainly focus on this essential area. Services are in actual PHP code, just php classes that extend the BaseRestService. As you might already know, a class can have multiple methods. In Pionia we call these Actions. Therefore, henceforth, the terms service and actions will be used for the same meaning throughout the same guide.

Creating a service

You can create a service using our pionia console or manually. All services, as a convention, must be located in the services folder.

We recommend to name your services after your database tables. Example, if your table is called ‘users’, you can name your service ‘UserService’.

If you are using our ‘pionia console’, then you can just name your service ‘user’. These are just conventions!

Remember generic services target a base table.

Therefore, you shall be asked the table you want to target. This is required.

However, starting from version 1.1.7, you can target relationships too!

You can read more about this in the Generic Services Section.

Service Registration

Creating a service is not enough in Pionia. You also need to register it in our switcher to make it discoverable by the kernel. Service registration happens in the associated switch.

In the switches folder, find the switch you want to use for this service. You can add your service as below.


 public function registerServices(): array
    {
        return [
            'user' => new UserService(),
            "todo" => TodoService::class, // this is okay
            'auth' => new AuthService(), // and this too
        ];
    }

The key of this method is the name you shall use in your proceeding requests to access this service. Therefore, it must be unique!

A single service can be registered in multiple switches. This is useful when you want to use the same service in different api versions.

Targeting a service in the request

In the request, you can target a service by determining the SERVICE key with your service name as the key defined in the registerServices method.

{
    SERVICE: 'user'
    // rest of your request data.
}

Targeting a service action

To target an action in a certain service, you need to define both the service and action as below.


{
    SERVICE: "user",
    ACTION: "loginAuth"
    // rest of your service data
}

The action in every request should match the name of your method in your service action. Pionia uses auto-discovery to automatically call the method passing in every required data needed for the request.

The keys SERVICE and ACTION are reserved and should not be used for any other purpose in the request data.

The same keys are case-sensitive and should be in uppercase.

Request Data and Response

Accessing the Request object in the services

You can access the request object in your service by calling the $this->request method on the service object. This returns the Pionia\Request\Request object. You can use this to access anything on the request.


class TodoService extends BaseRestService
{
    // your other actions here

    protected functon getTodo($data): BaseResponse
    {
        $request = $this->request;
        
        $uri = $this->request->getUri();

        // rest of actions logic

    }
}

Request Data

JSON and Form Request Data

An action takes $data as the first parameter which is an array of the request data. You can access you post data from this parameter.

This consists of both JSON and form data. Therefore, you can access your data as below.

    $username = $data["username"];
    $email = $data["email"];

You can also access your request data from the $this->request object.

    $allData = $this->request->getData();

You can also access the request method, headers, and other request data from the $this->request object.

    $method = $this->request->getMethod();
    $headers = $this->request->getHeaders();

Multipart Data(Uploads)

If your action expects multipart upload files, then you can get these from the second action parameter called $files. This is an associative array of all uploads.


protected function profileUpdates(array $data, ?array $files)
{
        $profilePic = $files['profile_pik'];
}

NOTE: This does not consist of base64 encoded uploads, for those, they’ll be part of the $data.

Action Response.

All actions must return a Pionia\Response\BaseResponse object. This is the object that is sent back to the client. You can use the BaseResponse object to send back a response with a status code, message, and data.

A helper method BaseResponse::JsonResponse is provided to help you create a response object that is ready to be serialized to JSON.

use Pionia\Response\BaseResponse;

class TodoService extends BaseRestService
{
    // your other actions here

    protected functon getTodo($data): BaseResponse
    {
        $this->mustAuthenticate();

        // rest of actions logic

        return BaseResponse::JsonResponse(200, 'Todo fetched successfully', $todo);
    }
}

For details about request and responses, you can check the request and response section.

Action protection

You can protect your actions by determining that they require only authenticated requests(users) to be accessed. You can do this in three ways.

Globally in the service

Entire service

You can mark an entire service as requiring authentication by setting the $serviceRequiresAuth parameter to true.


class TodoService extends BaseRestService
{
    public bool $serviceRequiresAuth = true; // all actions in this service require authentication.

    // your other actions here
}

Specific actions

You can also mark specific actions in a service as requiring authentication. Use the $actionsRequiringAuth parameter and add action names of actions that should be reached by authenticated users only.

This, unlike $serviceRequiresAuth, will only protect the actions listed in the array not the entire service.


class TodoService extends BaseRestService
{
    public bool $actionsRequiringAuth = ['getTodo'];

    // your other actions here
}

Internally in the action

You can also call the mustAuthenticate method anywhere in your action, preferably the first line in the action method.


class TodoService extends BaseRestService
{
    // your other actions here

    protected functon getTodo($data): BaseResponse
    {
        $this->mustAuthenticate(); // only authenticated will exceed this point.

        // rest of actions logic

    }
}

Details of how Pionia achieves authentication and authorization can be found in the authentication section.

Error Handling

According to Moonlight architecture, all requests should return a 200 Ok status code. This is because the client should be able to know if the request was successful or not by checking the returnCode in the response body.

All normal responses set this internally and are always returning a 200 status code. By convention and by default, all requests that are successful return 0 as the returnCode. This implies that the server can define multiple other return codes for other scenarios.

In Pionia, we have a global exception handler that catches all exceptions thrown anywhere in the code. This is to ensure that the client always gets the same response format.

All exceptions thrown are caught will raise a 500 status code and the message of the exception will be sent back to the client as the returnMessage.

Therefore, in your services and actions, you can throw exceptions as you see fit. And you don’t need to catch them at all!


protected function getTodo($data): BaseResponse
{
    $this->mustAuthenticate();

    if($data['id'] == null){
        throw new \Exception('Todo id is required'); // will be caught globally!
    }

    // rest of actions logic

    return BaseResponse::JsonResponse(200, 'Todo fetched successfully', $todo);
}

Please note that all exceptions are caught globally and sent back to the client. Therefore, you do not need to catch exceptions in your services. Developers need to set clean, descriptive exception messages in their exceptions to help the client understand what went wrong.

Deactivating actions in a service

BaseRestService provides a parameter $deactivatedActions that can be used to register all deactivated actions in a service. This is useful when you want to deactivate an action in a service without deleting it.

class TodoService extends BaseRestService
{
    public array $deactivatedActions = ['getTodo']; // one or more actions to deactivate.
   
}

Deactivated actions will not be called by the switcher. Therefore, they will not be accessible by the client.