{"id":15021851,"url":"https://github.com/shampine/sequence","last_synced_at":"2025-08-12T23:08:18.535Z","repository":{"id":45871452,"uuid":"293606310","full_name":"shampine/sequence","owner":"shampine","description":"Framework agnostic pipelining made simple.","archived":false,"fork":false,"pushed_at":"2023-04-17T23:31:06.000Z","size":739,"stargazers_count":21,"open_issues_count":4,"forks_count":2,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-07-14T07:07:12.442Z","etag":null,"topics":["laravel","laravel-pipeline","php","pipeline","symfony"],"latest_commit_sha":null,"homepage":"https://github.com/shampine/sequence-demo","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/shampine.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-09-07T18:43:30.000Z","updated_at":"2025-06-04T00:06:51.000Z","dependencies_parsed_at":"2025-02-18T00:31:39.694Z","dependency_job_id":"65d28a22-80b7-43af-b480-983dd177f22e","html_url":"https://github.com/shampine/sequence","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/shampine/sequence","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shampine%2Fsequence","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shampine%2Fsequence/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shampine%2Fsequence/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shampine%2Fsequence/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/shampine","download_url":"https://codeload.github.com/shampine/sequence/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shampine%2Fsequence/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":270149345,"owners_count":24535728,"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-12T02:00:09.011Z","response_time":80,"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":["laravel","laravel-pipeline","php","pipeline","symfony"],"created_at":"2024-09-24T19:57:08.480Z","updated_at":"2025-08-12T23:08:18.474Z","avatar_url":"https://github.com/shampine.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# sequence\n![example workflow name](https://github.com/shampine/sequence/workflows/Sequence%20Build/badge.svg)\n\nA framework agnostic pipelining package to handle complete requests built on [The PHP League Pipeline Package](https://github.com/thephpleague/pipeline).\n\n[Laravel Demo - https://github.com/shampine/sequence-demo](https://github.com/shampine/sequence-demo)  \n[Tutorial - https://medium.com/gosteady/day-5-sequence-how-to-guide-56c0af1b2303](https://medium.com/gosteady/day-5-sequence-how-to-guide-56c0af1b2303)  \n\n## why\n\nUsing the pipeline pattern developers can move quickly, recycle processes, and test everything.\n\n![pipeline diagram](https://raw.githubusercontent.com/shampine/sequence/master/diagram.png)\n\nBenefits to using pipelines for an MVC framework include\n\n - skinny and consistent controllers  \n - ability to share processes amongst different pipelines\n - simple injection of service or repository classes into the processes to keep code clean\n - ease of testing individual processes\n - clear, consistent api responses\n - eliminate need to try/catch exceptions inside the stack\n\n## installation\n\n`composer require shampine/sequence`\n\n## usage\n\nThese examples are using Laravel conventions but this package is framework agnostic.\n\nSee these three files for verbose usage examples and live demos inside the phpunit tests.\n\n[Sample Payload](https://github.com/shampine/sequence/blob/master/tests/Sample/SamplePayload.php)  \n[Sample Response](https://github.com/shampine/sequence/blob/master/tests/Sample/SampleResponse.php)  \n[Sample Pipeline](https://github.com/shampine/sequence/blob/master/tests/Sample/SamplePipeline.php)  \n\n### Payloads\n\nThis is the active workspace. The payload is mutated as it passes thru each stage. Any data needed from one\nstage to another needs to be set on the payload, and then retrieved from the payload.\n\nWhen defining your Payloads you can optionally define a `$allowlist` and `$overrides`.\n\n```php\n$allowlist = ['email']; // Only hydrate `email` from post/patch\n$overrides = ['email_address' =\u003e 'email']; // Allows `email_address` to be hydrated as `email`\n$payload = new \\Sample\\SamplePayload($allowlist, $overrides);\n```\n\nAn allowlist will limit what user supplied input will be hydrated into the Payload. The overrides parameter allows\nmapping of different external keys to internal keys. E.g. if the post contains `email_address` but on the payload the \nmethod is called `setEmail`. Mapping `['email_address' =\u003e 'email']` will properly align hydration.\n\n### Pipeline Composition\n\nA pipeline can have multiple named closures stored in the `$pipelines` property. This will allow grouping of similar\npipelines together, e.g. GET, POST, PATCH, DELETE pipelines. You can pass attributes into the pipeline either thru the\nclass constructor OR the closure constructor.\n\nServices, repositories, and other dependency injectable parameters are best set by using the class constructor. While\nflags and other stage related properties can be injected using `-\u003eprocess($pipelineName, $payload, ...$argments)`.\n\nThis example pipeline has a service injected into the constructor but two boolean flags passed through the $arguments\nparameter on `-\u003eprocess()`.\n\n```php\nclass SamplePipeline extends AbstractPipeline\n{\n    public const SAMPLE_PIPELINE = 'SamplePipeline';\n\n    public function __construct(?SampleUseService $sampleUseService = null)\n    {\n        $this-\u003epipelines = [\n            self::SAMPLE_PIPELINE =\u003e static function(\n                bool $validationFailure = false,\n                bool $sequenceFailure = false\n            ) use ($sampleUseService)\n            {\n                return (new Pipeline)\n                    -\u003epipe(new ValidationExceptionProcess($validationFailure, $sampleUseService))\n                    -\u003epipe(new SequenceExceptionProcess($sequenceFailure))\n                    -\u003epipe(new HydrateResponseProcess(SampleResponsePayload::class));\n            }\n        ];\n\n        $this-\u003eexcludeWhenEmpty = [\n            'empty_value',\n        ];\n\n        $this-\u003eexcludeWhenNull = [\n            'null_value',\n        ];\n    }\n}\n```\n\nThe property `$excludeWhenEmpty` or `$excludeWhenNull` will check ANY root or data keys to see if their value is\n`empty()` or `=== null`. If so they are excluded from the final array, all keys should use `snake_case`.\n\n### Response\n\nResponses are the final output containers and should be hydrated in the final stage of a pipeline. All properties\non the class can have a getter, but if they do not the property will be magically accessed.\n\n```php\npublic function __construct(SamplePayload $payload)\n{\n    $this-\u003esetSampleAbout('This is an about statement.');\n}\n```\n\nDuring the format process `getSampleAbout` would be used to compile the final array that will be returned as json.\n\n### Controller Examples (Laravel)\n\nUsing dependency injection on your controller to instantiate the pipeline.\n\n```php\nclass SampleController\n{\n    public function __construct(SamplePipeline $samplePipeline)\n    {\n        $this-samplePipeline = $samplePipeline;\n    }\n}\n```\n\n#### GET\n\n```php\npublic function get(Request $request)\n{\n    $payload = new SamplePayload();\n    $response = $this-\u003esamplePipeline-\u003eprocess(SamplePipeline::SAMPLE_PIPELINE, $payload)-\u003eformat();\n\n    return response()-\u003ejson($response, $response['http_code']);\n}\n```\n\n#### POST\n\n```php\npublic function post(Request $request)\n{\n    $payload = (new SamplePayload())-\u003ehydratePost($request-\u003eall());\n    $response = $this-\u003esamplePipeline-\u003eprocess(SamplePipeline::SAMPLE_PIPELINE, $payload)-\u003eformat();\n\n    return response()-\u003ejson($response, $response['http_code']);\n}\n```\n\n#### PATCH\nPatch payloads require the `PatchInterface` and `PatchTrait`. The payload will contain methods to decipher what\nis requested to be patched `-\u003egetPatch()` and whether the payload is a patch request `-\u003eisPatch()`.\n\n```php\npublic function patch(Request $request)\n{\n    $payload = (new SamplePayload())-\u003ehydratePatch($request-\u003eall());\n    $response = $this-\u003esamplePipeline-\u003eprocess(SamplePipeline::SAMPLE_PIPELINE, $payload)-\u003eformat();\n\n    return response()-\u003ejson($response, $response['http_code']);\n}\n```\n\n### Exceptions\n\nIncluded are two exceptions, ValidationException and SequenceException. Both are caught and rendered to json. You can\ndefine specific exception by extending these classes. They are caught and rendered the same as a normal payload to easily\nallow json to be return.\n\n```php\nclass FetchUser extends AbstractProcess\n{\n    public function process($payload)\n    {\n        $user = $this-\u003euserService-\u003egetUserRepository()-\u003efetchUserById($payload-\u003egetId());\n\n        if ($user === null) {\n            throw new SequenceException(1000, 'User not found', 400);\n        }\n    \n        $payload-\u003esetUser($user);\n\n        return $payload;\n    }\n}\n```\n\nA null user returns\n\n```json\n{\n  \"error_code\": 1000,\n  \"status_code\": 400,\n  \"data\": null,\n  \"message\": \"User not found\",\n  \"error_messages\": null\n}\n```\n\nAny standard php exception will fatal the application like normal. Logging should exist in a constructor of an exception\nclass that extends ValidationException or SequenceException.\n\n## testing\n\nTo run all tests run `./tests/run.sh`.\n\nThis will execute:\n\n - phpstan level 8\n - phpunit with code coverage (expects 100% coverage)\n\n## license\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshampine%2Fsequence","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fshampine%2Fsequence","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshampine%2Fsequence/lists"}