mkuettel/codeigniter4-services

Service Layer Library for CodeIgniter 4

v0.0.14 2024-05-09 13:42 UTC

This package is auto-updated.

Last update: 2024-05-09 22:42:02 UTC


README

pipeline status coverage report

This library should make it easier to facilitate a Service layer for CodeIgniter4 application. Additionally there are some tools to interact with the model layer, in particular this library contains a Service to have some nicer and more reliable syntax for database transactions.

Installation

You can add this library to your project using composer:

composer require mkuettel/codeigniter4-services

That's it, but you might want to change some of the configuration, as described in the next section.

Configuration

There are multiple configuration files available in the src/Config directory. You can copy these into your project and extend from the classes to override these default configurations.

(a publisher script will be added to copy these files to your project in the future)

Service Interface

This Library provides the interface \MKU\Services\ServiceInterface.

The idea is that you extend from this interface by defining another interface for your own service. In this interface you define all the methods your service will have:

use \MKU\Services\ServiceInterface;

interface MyBusinessService extends ServiceInterface {
    public function placeOrder(Order $order): Result;
    public function sendOrderConfirmationMail(Order $order): Result;
    // ...
}

You then implement this interface with your own service class.


class SimpleBusinessService extends MyBusinessService {
    
    private OrderModel $orderModel;
    private MailService $mailService;

    public function __construct(OrderModel $orderModel, MailService $mail) { /* ... */ }
    public function shortname(): string { return 'simple_business'; } 
    public function placeOrder(Order $order): Result { /* */ }
    public function sendOrderConfirmationMail(Order $order): Result { /* */ }
    // ...
}

You'll also need to implement shortname(): string method returning a unique identifier for this service implementation.

The shortname will later be used to get or create an instance the implementing service class using:

 service('<shortname>'); 
 \Config\Services::shortname();

But for this to work you must for now register it as follows

class Services extends BaseService {
    
    // ....
    
    // use the same parameters as required by the constructor of the service class,
    // but make them null by default.
    public function business_simple() (
        OrderModel $orderModel = null,
        MailService $mail = null,
        bool $getShared = true // add extra parameter to reuse past instance if available (e.g. a singleton instance)
    ): SimpleBusinessService {
        if ($getShared) return self::getSharedInstance('simple_business', $config, $db);
        return new SimpleBusinessService(
            $orderModel ?? model(OrderModel::class);
            $mail ?? self::mail_service(),
        );
    }
    
    // use the interface as return type here
    public function business(): MyBusinessService {
        // change which service class to use for the MyBusinessService interface here
        return self::business_simple();
    }
    
    // ....
}

TODO(so this must not be done manually): add ServiceContainer as a base class for \Config\Services which autoconfigures the services depending on their constructor types or annotations and creates the builder methods in \Config\Services.

Transaction Service

This library provides a TransactionService, which allows you to execute a custom function during a database transaction:

// PHP 7.4+ using arrow functions
service('transaction')->transact(fn() => do_database_operation())

// Using anonymous function (or any Closure)
service('transaction')->transact(function() use ($db) {
    $db->insert( /* update */ ); ...
    $db->update( /* ...  */); ...
});

Used in this manner, the transaction service begins a transaction before executing the given closure. If an exception occurs during the transaction, the transaction will be rolled back.

Nested Transactions

Transactions can be nested if the underlying database driver and database support them. Currently these are not fully implemented for all drivers, but this Repository provides a a replacement which extends the CI4 database drivers for SQLite & MySQLi with support for nested transactions by using savepoints.

You can use them by setting the DBDriver option in your .env file or Database configuration to:

# .env
# ...
# use mysqli driver with support for nested transactions
database.default.DBDriver = '\MKU\Services\Database\Transactional\MySQLi'
# ...
# use SQLite3 driver with support for nested transactions
database.tests.DBDriver = '\MKU\Services\Database\Transactional\SQLite3'

Additionally, different drivers and databases may have different transaction guarantees and isolation levels. Also watch out for autocommit behaviour, which may be enabled by default in some database systems.

DataProviders and PersistenceServices

This library also provides a DataProvider interface and a PersistenceService interface. These interfaces are meant to be used in the service layer to interact with the model layer. The idea is to separate the data access layer (models, CodeIgniter\Database stuff) from the business logic layer (Controllers), by only returning entity objects and no database connections handles, query result objects or other database specific objects.

In order for the persistence services to identify and address entities correctly entity classes used as parameters for data provider or persistence services must implement the ServiceEntity interface, forcing you to define a primary key (or to declare that the entity has none). If you have a entity class which extends form the CodeIgniter Entity (which is no requirement) class you can use the CodeIgniterServiceEntityTrait to use the defined $primaryKey property.

Data providers are services which query and provide data as entity objects, but itself do not modify the data. The following three basic methods are provided by the DataProvider interface:

interface DataProvider {
    public function get($id): ?ServiceEntity;
    public function refresh(ServiceEntity $id): ?bool;
    public function exists($id): bool;
}

$id is the primary key of the entity to be queried, this can also be an array of multiple attributes. You can define this by implementing the ServiceEntity in your entity class.

Persistence services are services are an extension of data providers and also need to implement methods to modify the data they provide. The following additional methods are provided:

interface PersistenceService extends DataProvider {
    public function save(ServiceEntity $entity): Result;
    public function delete($id): ServiceEntity|bool;
}

Configurable Services

Currently theres just a the MKU\Services\Library\Config\Configurable interface and the ConfigurableTrait in the same namespace, simplifying the implementation of the former interface.

The idea is to that you use the Trait and implement the applyConfig() method in your service to configure it using your own custom codeigniter Config class.

This feature is not yet clearly thought out completely and is very experimental and may be subject to change.

Contributing

Send me a message if you want to contribute to this project. I'm happy to receive feedback and suggestions.

Pull requests and bug reports are welcome at my GitHub repository:

https://github.com/mkuettel/codeigniter4-services