{"id":15026100,"url":"https://github.com/headio/phalcon-service-layer","last_synced_at":"2025-06-11T08:39:03.315Z","repository":{"id":62515234,"uuid":"236178190","full_name":"headio/phalcon-service-layer","owner":"headio","description":"A service layer implementation for Phalcon projects","archived":false,"fork":false,"pushed_at":"2023-04-17T15:35:18.000Z","size":279,"stargazers_count":3,"open_issues_count":2,"forks_count":2,"subscribers_count":1,"default_branch":"5.x","last_synced_at":"2025-05-15T13:00:28.877Z","etag":null,"topics":["phalcon","phalcon-orm","phalcon-php","phalcon4","phalcon5","php72","php73","php74","php8","php80","repository","service-layer"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/headio.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-01-25T14:06:50.000Z","updated_at":"2023-03-03T13:31:26.000Z","dependencies_parsed_at":"2025-02-15T20:32:44.661Z","dependency_job_id":"2feb0cc6-2ff0-4e3a-8c8a-0ff8caf51839","html_url":"https://github.com/headio/phalcon-service-layer","commit_stats":null,"previous_names":[],"tags_count":28,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/headio%2Fphalcon-service-layer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/headio%2Fphalcon-service-layer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/headio%2Fphalcon-service-layer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/headio%2Fphalcon-service-layer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/headio","download_url":"https://codeload.github.com/headio/phalcon-service-layer/tar.gz/refs/heads/5.x","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/headio%2Fphalcon-service-layer/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259231366,"owners_count":22825568,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["phalcon","phalcon-orm","phalcon-php","phalcon4","phalcon5","php72","php73","php74","php8","php80","repository","service-layer"],"created_at":"2024-09-24T20:03:44.933Z","updated_at":"2025-06-11T08:39:03.262Z","avatar_url":"https://github.com/headio.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Phalcon service layer\n\nA simple repository service implementation for Phalcon 5 projects\n\n[![Build Status](https://travis-ci.com/headio/phalcon-service-layer.svg?branch=5.x)](https://travis-ci.com/headio/phalcon-service-layer) [![Coverage Status](https://coveralls.io/repos/github/headio/phalcon-service-layer/badge.svg?branch=5.x)](https://coveralls.io/github/headio/phalcon-service-layer?branch=5.x)\n\n## Introduction\n\nThis library provides a layered architecture promising easier unit and integration testing.\n\nThe service layer handles business logic, mediating between the application layer (controller or handler) and the domain, interacting with a single repository or multiple repositories. All repositories extend an abstract **query repository**, providing a collection-like interface, with well-defined query methods. Hence all queries are isolated in the repository layer.\n\nPhalcon ORM implements the active record pattern, therefore the responsiblity of persistence remains with the active record, in contrast to the repository service pattern / data mapper (Doctrine), where repositories manage the entity lifecycle.\n\nIf you have been reading between the lines, you have probably gathered this is a hybrid solution offering: testability, reuseability and prevention of logic leaking into the application layer. The trade-off is you need to write, test and maintain some extra boiler-plate code.\n\nNaturally, you can avoid this paradigm by integrating a data mapper (Doctrine, Atlas ORM etc.) with Phalcon. Nevertheless, for those enjoying the performance of Phalcon ORM, this library may be of interest.\n\n## Dependencies\n\n* PHP \u003e=8.0.0 \u003c=8.0.99\n* Phalcon 5.0.0+\n\n## Installation\n\n### Composer\n\nOpen a terminal window and run:\n\n```bash\ncomposer require headio/phalcon-service-layer\n```\n\n## Usage\n\nAssuming the following project structure, let's create the layers to handle removing a record from storage as a simple usage example.\n\n```bash\n├── src\n│   │── Module\n│   │    │── Admin\n│   │    │    │── Controller\n│   │    │    │    │── Foo\n│   │    │    │── Module.php\n│   │── Domain\n│   │    │── Model\n│   │    │    │── Foo\n│   │    │── Repository\n│   │    │    │── Foo\n│   │    │── Service\n│   │    │    │── Foo\n│   │── Provider\n│   │    │── FooService\n└──\n```\n\n### Registering a service provider\n\nCreate a new **Foo** service dependency inside the service provider directory **/src/Provider/**.\n\n```php\ndeclare(strict_types=1);\n\nnamespace App\\Service;\n\nuse App\\Domain\\Repository\\Foo as Repository;\nuse App\\Domain\\Service\\Foo as Service;\nuse Headio\\Phalcon\\ServiceLayer\\Repository\\Factory;\nuse Phalcon\\Di\\ServiceProviderInterface;\nuse Phalcon\\Di\\DiInterface;\n\nclass Foo implements ServiceProviderInterface\n{\n    public function register(DiInterface $di): void\n    {\n        $di-\u003esetShared(\n            'fooService',\n            function () {\n                $repository = Factory::create(Repository::class)\n                $service = new Service($repository);\n\n                return $service;\n            }\n        );\n    }\n}\n```\n\nAlternatively, create the dependency on a per-module basis.\n\n```php\ndeclare(strict_types=1);\n\nnamespace App\\Module\\Admin;\n\nuse App\\Domain\\Repository\\Foo as Repository;\nuse App\\Domain\\Service\\Foo as Service;\nuse Headio\\Phalcon\\ServiceLayer\\Repository\\Factory;\nuse Phalcon\\Di\\DiInterface;\nuse Phalcon\\Loader;\nuse Phalcon\\Mvc\\ModuleDefinitionInterface;\n\nclass Module implements ModuleDefinitionInterface\n{\n    /**\n     * {@inheritDoc}\n     */\n    public function registerAutoloaders(DiInterface $container = null)\n    {\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    public function registerServices(DiInterface $container)\n    {\n        $container-\u003esetShared(\n            'fooService',\n            function () use ($container) {\n                $repository = Factory::create(Repository::class);\n                $service = new Service($repository);\n\n                return $service;\n            }\n        );\n    }\n}\n```\n\n### Controller/Handler\n\nNow the service is in place, the controller can interact with the service layer by injecting the service into the controller via the **OnConstruct** method.\n\n```php\nnamespace App\\Module\\Admin\\Foo;\n\nuse Phalcon\\Mvc\\Controller;\n\nclass Foo extends Controller\n{\n    private FooInterface $service;\n\n    /**\n     * Inject service layer dependencies\n     */\n    public function onConstruct(): void\n    {\n        $this-\u003eservice = $this-\u003egetDI()-\u003eget('fooService');\n    }\n}\n```\n\n### Service layer\n\nThe service layer interacts with one repository (or multiple repositories) to process the business logic. In the example below, the service calls the delete method (implementation skipped for simplicity) to remove a model instance by primary key and return to the list view.\n\n```php\ndeclare(strict_types=1);\n\nnamespace App\\Domain\\Service\\Foo;\n\nuse App\\Domain\\Repository\\FooInterface;\nuse App\\Domain\\Service\\FooInterface as ServiceInterface;\nuse Phalcon\\Di\\Injectable;\nuse Phalcon\\Http\\ResponseInterface;\n\nclass Foo extends Injectable implements ServiceInterface\n{\n    public function __construct(private FooInterface $repository)\n    {\n    }\n\n    /**\n     * Delete a model instance\n     */\n    public function deleteModel(int $id): ResponseInterface\n    {\n        $model = $this-\u003erepository-\u003efindByPk($id);\n\n        if ($this-\u003edelete($model)) {\n            $this-\u003eflashSession-\u003enotice('Task completed');\n            return $this-\u003eresponse-\u003eredirect(['for' =\u003e 'adminFoos']);\n        }\n    }\n}\n```\n\n### Repository\n\nAll repositories must extend the abstract query repository and implement one abstract method.\n\n```php\ndeclare(strict_types=1);\n\nnamespace App\\Domain\\Repository;\n\nuse App\\Domain\\Model\\Foo as Model;\nuse App\\Domain\\Repository\\FooInterface;\nuse Headio\\Phalcon\\ServiceLayer\\Repository\\QueryRepository;\n\nclass Foo extends QueryRepository implements FooInterface\n{\n    /**\n     * Return the model name managed by this repository.\n     */\n    protected function getModelName(): string\n    {\n        return Model::class;\n    }\n}\n```\n\nThe **Foo** repository can implement additional interfaces, e.g. **FooInterface**, providing further concrete methods for the service layer.\n\nThe abstract query repository implements the following repository interface:\n\n```php\ndeclare(strict_types=1);\n\nnamespace Headio\\Phalcon\\Repository\\Repository;\n\nuse Headio\\Phalcon\\ServiceLayer\\Model\\CriteriaInterface;\nuse Headio\\Phalcon\\ServiceLayer\\Model\\ModelInterface;\nuse Phalcon\\Mvc\\Model\\ResultsetInterface;\nuse Phalcon\\Mvc\\Model\\Query\\BuilderInterface;\n\ninterface RepositoryInterface\n{\n    /**\n     * Return an instance of the query criteria pre-populated\n     * with the model managed by this repository.\n     */\n    public function createCriteria(): CriteriaInterface;\n\n    /**\n     * Return an instance of the query builder.\n     */\n    public function createBuilder(array $params = null): BuilderInterface;\n\n    /**\n     * Fetch a column value by query criteria from storage.\n     */\n    public function fetchColumn(CriteriaInterface $criteria): mixed;\n\n    /**\n     * Fetch records by query criteria from storage.\n     */\n    public function find(CriteriaInterface $criteria): ResultsetInterface;\n\n    /**\n     * Fetch record by primary key from storage.\n     */\n    public function findByPk(int $id): ModelInterface;\n\n    /**\n     * Fetch first record by query criteria from storage.\n     */\n    public function findFirst(CriteriaInterface $criteria): ModelInterface;\n\n    /**\n     * Fetch first record by property name from storage.\n     */\n    public function findFirstBy(string $property, mixed $value): ModelInterface;\n\n    /**\n     * Return the fully qualified (or unqualified) class name\n     * for the model managed by this repository.\n     */\n    public function getModel(bool $unqualified = false): string;\n}\n```\n\nIn addition, a relationship trait is provided to simplify handling model relationships.\n\n```php\ndeclare(strict_types=1);\n\nnamespace App\\Domain\\Repository;\n\nuse App\\Domain\\Model\\Foo as Model;\nuse App\\Domain\\Repository\\FooInterface;\nuse Headio\\Phalcon\\ServiceLayer\\Repository\\QueryRepository;\nuse Headio\\Phalcon\\ServiceLayer\\Repository\\Traits\\RelationshipTrait;\n\nclass Foo extends QueryRepository implements FooInterface\n{\n    use RelationshipTrait;\n\n    /**\n     * Return the model name managed by this repository.\n     */\n    protected function getModelName(): string\n    {\n        return Model::class;\n    }\n}\n```\n\n#### Query caching\n\nQuery caching is handled utilizing Phalcon's event manager.\nTo get started first include the **CacheableTrait** in your repository; the **EventsAwareInterface** is implemented inside the cacheable trait.\n\n```php\ndeclare(strict_types=1);\n\nnamespace App\\Domain\\Repository;\n\nuse App\\Domain\\Model\\User as Model;\nuse Headio\\Phalcon\\ServiceLayer\\Model\\ModelInterface;\nuse Headio\\Phalcon\\ServiceLayer\\Repository\\QueryRepository;\nuse Headio\\Phalcon\\ServiceLayer\\Repository\\Traits\\CacheableTrait;\nuse Phalcon\\Events\\EventsAwareInterface;\n\nclass User extends QueryRepository implements UserInterface, EventsAwareInterface\n{\n    use CacheableTrait;\n\n    /**\n     * Return the model name managed by this repository.\n     */\n    protected function getModelName(): string\n    {\n        return Model::class;\n    }\n}\n```\n\nThen create a service provider for your service layer, or a repository if you want to omit the service layer and work with repositories directly. The example below utilizes Phalcon's service provider interface.\n\n```php\ndeclare(strict_types=1);\n\nnamespace App\\Service;\n\nuse App\\Domain\\Repository\\Foo as Repository;\nuse App\\Domain\\Service\\Foo as Service;\nuse Headio\\Phalcon\\ServiceLayer\\Cache\\Listener\\CacheListener;\nuse Headio\\Phalcon\\ServiceLayer\\Repository\\Factory;\nuse Phalcon\\Di\\ServiceProviderInterface;\nuse Phalcon\\Di\\DiInterface;\n\nclass Foo implements ServiceProviderInterface\n{\n    public function register(DiInterface $di): void\n    {\n        $di-\u003esetShared(\n            'fooService',\n            function () {\n                $eventsManager = new EventsManager();\n                // factory instantiation\n                $repository = Factory::create(Repository::class);\n                $repository-\u003esetEventsManager($eventsManager);\n                $cacheManager = $this-\u003eget('cacheManager');\n                // attach the cache event listener and inject the\n                // cache manager dependency\n                $eventsManager-\u003eattach(\n                    'cache',\n                    new CacheListener(\n                        $cacheManager\n                    )\n                );\n                $service = new Service($repository);\n\n                return $service;\n            }\n        );\n    }\n}\n```\n\n##### Cache event listener\n\nThe event listener provides two methods to handle caching, see below.\n\n```php\n/**\n * This event listener provides caching functionality for repositories.\n */\nclass CacheListener\n{\n    public function __construct(private ManagerInterface $manager)\n    {\n    }\n\n    /**\n     * Appends a cache declaration to a Phalcon query instance.\n     */\n    public function append(\n        EventInterface $event,\n        RepositoryInterface $repository,\n        QueryInterface $query,\n    ): QueryInterface;\n\n    /**\n     * Fetches data from cache or storage using the cache-aside\n     * strategy.\n     */\n    public function fetch(\n        EventInterface $event,\n        RepositoryInterface $repository,\n        array $context,\n    ): ModelInterface|ResultsetInterface;\n}\n```\n\nTo trigger a cache event, see the following concrete examples from the cacheable trait.\n\n```php\ntrait CacheableTrait\n{\n    /**\n     * Fetch first record by query criteria from cache or storage.\n     *\n     * @throws NotFoundException\n     */\n    public function findFirst(CriteriaInterface $criteria): ModelInterface\n    {\n        $query = $criteria\n            -\u003ecreateBuilder()\n            -\u003egetQuery()\n            -\u003esetUniqueRow(true)\n        ;\n        $this-\u003eeventsManager-\u003efire('cache:append', $this, $query);\n        $model = $query-\u003eexecute();\n\n        if (!$model instanceof ModelInterface) {\n            throw new NotFoundException('404 Not Found');\n        }\n\n        return $model;\n    }\n\n    /**\n     * Fetch data from cache or storage.\n     */\n    public function fromCache(\n        QueryInterface|array $query,\n        Closure $callable,\n        DateInterval|int $lifetime = null,\n    ): ResultsetInterface|ModelInterface|null {\n        $key = $this-\u003ecacheManager-\u003egenerateKey(\n            $this-\u003egetModel(),\n            $query,\n        );\n\n        return $this-\u003eeventsManager-\u003efire(\n            'cache:fetch',\n            $this,\n            [$key, $callable],\n        );\n    }\n```\n\n### Pagination\n\nThis library provides a cursor-based paginator adapter; see **_stub** directory inside the test directory for usage.\n\n### The model\n\nAll models must extend the abstract **Model** class, which implements the following model interface:\n\n```php\ndeclare(strict_types=1);\n\nnamespace Headio\\Phalcon\\ServiceLayer\\Model;\n\nuse Phalcon\\Di\\DiInterface;\nuse Phalcon\\Mvc\\Model\\CriteriaInterface;\n\ninterface ModelInterface\n{\n    /**\n     * Return the model primary key attribute.\n     */\n    public function getPrimaryKey(): string;\n\n    /**\n     * Return the property binding type for a given property.\n     */\n    public function getPropertyBindType(string $property): int;\n\n    /**\n     * Return the model validation errors as an array representation.\n     */\n    public function getValidationErrors(): array;\n\n    /**\n     * Return an instance of the query criteria pre-populated\n     * with the model.\n     */\n    public static function query(DiInterface $container = null): CriteriaInterface;\n}\n```\n\n### Validation\n\nValidation can be implemented in the service layer or the model classes.\n\n## Testing\n\nTo see the tests, run:\n\n```bash\nphp vendor/bin/codecept run -f --coverage --coverage-xml\n```\n\n## License\n\nPhalcon service layer is open-source and licensed under [MIT License](http://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fheadio%2Fphalcon-service-layer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fheadio%2Fphalcon-service-layer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fheadio%2Fphalcon-service-layer/lists"}