{"id":23163526,"url":"https://github.com/icolomina/api_one_endpoint","last_synced_at":"2025-08-18T03:32:09.419Z","repository":{"id":65726328,"uuid":"595118447","full_name":"icolomina/api_one_endpoint","owner":"icolomina","description":"Create operation oriented API's ","archived":false,"fork":false,"pushed_at":"2024-04-17T13:43:37.000Z","size":203,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-05T19:22:52.501Z","etag":null,"topics":["php","symfony"],"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/icolomina.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}},"created_at":"2023-01-30T12:41:15.000Z","updated_at":"2025-03-05T17:38:42.000Z","dependencies_parsed_at":"2024-04-17T14:59:28.344Z","dependency_job_id":null,"html_url":"https://github.com/icolomina/api_one_endpoint","commit_stats":{"total_commits":35,"total_committers":1,"mean_commits":35.0,"dds":0.0,"last_synced_commit":"c096448f3e43280a036d68e8f2227111c5ddbb73"},"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/icolomina/api_one_endpoint","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icolomina%2Fapi_one_endpoint","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icolomina%2Fapi_one_endpoint/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icolomina%2Fapi_one_endpoint/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icolomina%2Fapi_one_endpoint/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/icolomina","download_url":"https://codeload.github.com/icolomina/api_one_endpoint/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icolomina%2Fapi_one_endpoint/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":270940378,"owners_count":24671669,"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","status":"online","status_checked_at":"2025-08-18T02:00:08.743Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["php","symfony"],"created_at":"2024-12-18T00:18:47.212Z","updated_at":"2025-08-18T03:32:09.026Z","avatar_url":"https://github.com/icolomina.png","language":"PHP","readme":"![ci status](https://github.com/icolomina/api_one_endpoint/actions/workflows/ci.yml/badge.svg)\n\n# api_one_endpoint\n\nApi one endpoint bundle allows you to create an api focusing on operations instead of resources. One endpoint is used as a resource and api looks into payload \nto extract what operation has to perform and what data needs to perform it.\n\n### Installation\n\nUse composer to install this bundle:\n```shell\ncomposer require ict/api_one_endpoint:^1.0\n```\n\n### Inputs and outputs\n\nInputs and outputs define operations I/O flow. Each operatios can require an input (if it needs data to perform it) and must define an output which will be returned \nback to the client. \nInput have to be sent as a POST Json request and must follow the next schema:\n\n```json\n   {\n     \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n     \"type\": \"object\",\n     \"properties\": {\n       \"operation\": {\n         \"type\": \"string\"\n       },\n       \"data\": {\n         \"type\": \"object\"\n       }\n     },\n     \"required\": [\n       \"operation\",\n       \"data\"\n     ]  \n   }\n```\nAs an example, let's see how a SendPaymentOperation input could looks like:\n\n```json\n   {\n      \"operation\" : \"SendPaymentOperation\",\n      \"data\" : {\n        \"from\" : \"xxxx\",\n        \"to\" : \"yyyy\",\n        \"amount\" : 6.35\n      }\n   }\n```\n\nAs we can seein the above payload, we're sending an input which tells our api to perform _SendPaymentOperation_ and use as data the following keys:\n- **from**: Who sends payment\n- **To**: Who receives payment\n- **Amount**: How much money _xxxx_ sends to _yyyy_\n\nOutputs must be wrapped with an _Ict\\ApiOneEndpoint\\Model\\Api\\ApiOutput_ object. As a parameters, ApiOutput receives data which will be returned to client \nand http code which will be used in the response. Data can be an iterable (if we want to return a collection of data) or an object. This bundles relies \non symfony serializer to send a json response to the client.\n\nLet's see the following examples:\n\n```php\n   \n   class Hero {\n      public function __construct(\n         public readonly string $name,\n         public readonly string $hero\n      ){ }\n   }\n\n   $data = [\n      new Hero('Peter Parker', 'spiderman'),\n      new Hero('Clark Kent', 'superman')\n   ];\n   \n   return new ApiOutput($data, 200);\n```\n\n```php\n   class PaymentDoneOutput {\n       public function __construct(\n          public readonly string paymentId,\n          public readonly \\DateTimeInmutable $date\n       ){ }\n   }\n   \n   $paymentOutput = new PaymentDoneOutput('899875557', new \\DateTimeInmutable('2023-03-05 12:25');\n   return new ApiOutput($paymentOutput, 202);\n```\n\nFirst example returns an array of Hero objects as an output and second example returns PaymentDoneOutput object.\n\nIf you want to use [symfony serializer groups](https://symfony.com/doc/current/serializer.html#using-serialization-groups-attributes) in your outputs, you can use\nthe third ApiOutput parameter to pass the group name\n\n```php\nreturn new ApiOutput($paymentOutput, 202, 'admin');\n```\n\n### Defining input operations\n\nOperation inputs must be defined creating simple objects with its getters and setters. You can use [symfony validation constraints](https://symfony.com/doc/current/reference/constraints.html) to define validation rules so your input must hold required and valid data. This bundle \nwill validate input automatically and will throw an Ict\\ApiOneEndpoint\\Exception\\OperationValidationException if validation fails.\n\nAs an example, let's see how a payment operation input would looks like:\n\n```php\nuse Symfony\\Component\\Validator\\Constraints\\GreaterThan;\nuse Symfony\\Component\\Validator\\Constraints\\NotBlank;\n\nclass SendPaymentOperationInput\n{\n    #[NotBlank(message: 'From cannot be empty')]\n    private string $from;\n\n    #[NotBlank(message: 'To cannot be empty')]\n    private string $to;\n\n    #[NotBlank(message: 'Amount cannot be empty')]\n    #[GreaterThan(0, message: 'Amount must be greater then 0')]\n    private string $amount;\n    \n    // getters \u0026 setters\n\n}\n```\n\n### Defining operations\n\nOperations must implement Ict\\ApiOneEndpoint\\Contract\\Operation\\OperationInterface\n\n```php\nuse Ict\\ApiOneEndpoint\\Contract\\Operation\\OperationInterface;\nuse Ict\\ApiOneEndpoint\\Model\\Api\\ApiOutput;\n\nclass SendPaymentOperation implements OperationInterface\n{\n\n    public function perform(mixed $operationData): ApiOutput\n    {\n        // Perform operation ....\n        // Sending bizum, BTC ......\n        return new ApiOutput([], 200);\n    }\n\n    public function getName(): string\n    {\n        return 'SendPayment';\n    }\n\n    public function getInput(): ?string\n    {\n        return SendPaymentOperationInput::class;\n    }\n\n    public function getGroup(): ?string\n    {\n        return null;\n    }\n    \n    public function getContext() : ?array\n    {\n        return null;\n    }\n}\n```\n\nEach operation must define the following methods (declared in OperationInterface):\n\n- **perform**: Executes operation\n- **getName**: Gets operation name\n- **getInput**: Gets input class. When an operation is executed, payload data will be deserialized to the input class and also validated. \n- **getGroup**: Gets operation group. We will cover it when seeing operation authorization.\n- **getContext**: Gets the operation context. We will cover it when seeing operation context separation\n\n### Protecting operations\n\nThis bundle relies on [symfony voters](https://symfony.com/doc/current/security/voters.html) to protect operations. Take a look to the following voter:\n\n```php\nuse Ict\\ApiOneEndpoint\\Model\\Operation\\OperationSubject;\nuse Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface;\nuse Symfony\\Component\\Security\\Core\\Authorization\\Voter\\Voter;\n\nclass SendPaymentVoter extends Voter\n{\n\n    protected function supports(string $attribute, mixed $subject): bool\n    {\n        if(!$subject instanceof OperationSubject){\n            return false;\n        }\n\n        return true;\n    }\n\n    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool\n    {\n        $user = $token-\u003egetUser();\n        if(!in_array('ROLE_PAYMENT', $user-\u003egetRoles())){\n            return false;\n        }\n\n        return true;\n    }\n}\n```\n\n\u003e If you want to protect you api from an autentication context (JWT token, user / pass) you can use [symfony security](https://symfony.com/doc/current/security/custom_authenticator.html)\n\nThe bundle pass an Ict\\ApiOneEndpoint\\Model\\Operation\\OperationSubject as an attribute to the voter. This gives you access to operation name and \noperation group (if it's been defined in getGroup method). Groups could be useful when you want to grant access to a group of operations to certain role or roles.\nAs an example, let's imagine you have the following operations:\n\n- CreateAccount\n- UpdateAccount\n- RemoveAccount\n\nIf you would want to restrict access to admin role, you would have to return the same group in getGroup method and then check the group in your voter.\n\n```php\nuse Ict\\ApiOneEndpoint\\Model\\Operation\\OperationSubject;\nuse Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface;\nuse Symfony\\Component\\Security\\Core\\Authorization\\Voter\\Voter;\n\nclass AccountManagementVoter extends Voter\n{\n\n    protected function supports(string $attribute, mixed $subject): bool\n    {\n        if(!$subject instanceof OperationSubject){\n            return false;\n        }\n\n        return $subject-\u003egroup === 'ACCOUNT';\n    }\n\n    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool\n    {\n        $user = $token-\u003egetUser();\n        if(!in_array('ROLE_ADMIN', $user-\u003egetRoles())){\n            return false;\n        }\n\n        return true;\n    }\n}\n```\nThe operation subject contains the following information (as public and readonly properties):\n\n- **operation**: Fully qualified operation class name\n- **operationName**: Operation Name (Value returned by method *getName*)\n- **group**: Operation group (Value returned by method *getGroup*)\n- **data**: Data to perform operation\n\n\u003e This bundle always check operation authorization by it's possible there are no voters defined or no voters supporting conditions. To avoid getting denied access when no voters executed, set the following access decision strategy in your security.yaml file:\n\n```yaml\nsecurity:\n   ......\n   access_decision_manager:\n      strategy: unanimous\n      allow_if_all_abstain: true\n```\n\nYou can find more information about access decision strategy in [symfony docs](https://symfony.com/doc/current/security/voters.html#changing-the-access-decision-strategy)\n\n### Sending operations to the background\n\nIn order to allow developers to configure some operations to be executed in the background, this bundle relies on:\n\n- [Symfony messenger](https://symfony.com/doc/current/messenger.html)\n- **Ict\\ApiOneEndpoint\\Model\\Attribute\\IsBackground** attribute\n\nLet's go back to SendPayment operation\n\n```php\nuse Ict\\ApiOneEndpoint\\Contract\\Operation\\OperationInterface;\nuse Ict\\ApiOneEndpoint\\Model\\Api\\ApiOutput;\nuse Ict\\ApiOneEndpoint\\Model\\Attribute\\IsBackground;\n\n#[IsBackground]\nclass SendPaymentOperation implements OperationInterface\n{\n   .......\n}\n```\nWhen an operation is annotated with **IsBackground** attribute, it's execution will be performed in the background and the client will no have to wait until it finishes. It can be useful for operations which can take more time to finish. Sending an email or a payment would be examples of this kind of operations.\n\nAfter an operation is sent to the background, a 202 (Accepted) http request is returned to the client with the following content:\n\n```json\n  {\n     \"status\" : \"QUEUED\"\n  }\n```\nIf you want to delay the execution some time, you can add the delay property to the attribute:\n\n```php\nuse Ict\\ApiOneEndpoint\\Contract\\Operation\\OperationInterface;\nuse Ict\\ApiOneEndpoint\\Model\\Api\\ApiOutput;\nuse Ict\\ApiOneEndpoint\\Model\\Attribute\\IsBackground;\n\n#[IsBackground(delay: 300)]\nclass SendPaymentOperation implements OperationInterface\n{\n   .......\n}\n```\n\nIt will delay the execution 300 seconds (5 minutes).\n\nLet's see how you should configure your messenger.yaml file to queue operations:\n\n```yaml\nmessenger:\n  transports:\n      your_transport_name: \"%env(MESSENGER_TRANSPORT_DSN)%\"\n\n      routing:\n        'Ict\\ApiOneEndpoint\\Message\\OperationMessage': your_transport_name\n```\n\nWith the above configuration, you will be able to route operations to your transport.\n\n### Events\n\nAfter an operation is performed, this bundle dispatches an \\Ict\\ApiOneEndpoint\\EventSubscriber\\Event\\OperationPerformedEvent so the developer can listen to it and execute some task, for instance sending a notification to the user\n\n```php\n\nclass OperationSubscriber implements EventSubscriberInterface {\n\n    public static function getSubscribedEvents(): array\n    {\n        return [\n            OperationPerformedEvent::class =\u003e ['onOperationPerformed']    \n        ]\n\n    }\n    \n    public function onOperationPerformed(OperationPerformedEvent $event): void\n    {\n         // events gives you access to operation name and operation result:\n         $opName   = $event-\u003eoperation;\n         $opResult = $event-\u003eoperationResult;\n         \n         // some stuff here .....\n    }\n}\n```\n\nIf you are interested on sending notifications to the user, consider using this [symfony notifier](https://symfony.com/doc/current/notifier.html)\n\n### Operations contexts\n\nAs we can see before, *OperationInterface* has a method *getContext* which can returns an array holding the allowed contexts or null. Let's imagine we want to keep two endpoints:\none for clients and another one for providers. If we want an operation to allow only client context, we would write *getContext* method as follows:\n\n```php\npublic function getContext(): ?array\n{\n    return ['client'];\n}\n```\n\nIn the next section we will se how to set up an endpoint context.\n\n### The controller\n\nSetting up your controller is a really easy task. Let's take a look\n\n```php\n\nuse Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;\nuse Ict\\ApiOneEndpoint\\Operation\\OperationHandler;\nuse Symfony\\Component\\HttpFoundation\\JsonResponse;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse Symfony\\Component\\Routing\\Annotation\\Route;\nuse Symfony\\Component\\Serializer\\SerializerInterface;\nuse Ict\\ApiOneEndpoint\\Model\\Api\\Context;\n\n#[Route('/api/v1')]\nclass BackendController extends AbstractController\n{\n    use \\Ict\\ApiOneEndpoint\\Controller\\OperationControllerTrait;\n\n    #[Route('', name: 'api_backend_v1_process_operation', methods: ['POST'])]\n    public function processOperation(Request $request, SerializerInterface $serializer, OperationHandler $operationHandler): JsonResponse\n    {\n        return $this-\u003eexecuteOperation($request, $serializer, $operationHandler, new Context());\n    }\n}\n```\n\nYou simply have to create your controller and use trait _\\Ict\\ApiOneEndpoint\\Controller\\OperationControllerTrait_. Then use the method _executeOperation_ passing to it $request, $serializer and $operationHandler as an arguments and your operation will be executed.\nWhat about we want to limit our endpoint to manage only client context operations? We only would have to pass the context name to the *Context* object constructor\n\n```php\nreturn $this-\u003eexecuteOperation($request, $serializer, $operationHandler, new Context('client'));\n```\n\nNow, this controller would only execute client context operations.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ficolomina%2Fapi_one_endpoint","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ficolomina%2Fapi_one_endpoint","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ficolomina%2Fapi_one_endpoint/lists"}