{"id":24774976,"url":"https://github.com/shootphp/shoot","last_synced_at":"2025-10-11T22:52:26.582Z","repository":{"id":49646918,"uuid":"113207024","full_name":"shootphp/shoot","owner":"shootphp","description":"Shoot aims to make providing data to your templates more manageable","archived":false,"fork":false,"pushed_at":"2023-09-01T13:29:16.000Z","size":125,"stargazers_count":41,"open_issues_count":0,"forks_count":14,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-10-11T22:52:25.440Z","etag":null,"topics":["php","php7","shoot","twig","twig-extension"],"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/shootphp.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","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":"2017-12-05T16:35:14.000Z","updated_at":"2023-08-21T06:26:11.000Z","dependencies_parsed_at":"2024-06-19T04:01:27.696Z","dependency_job_id":null,"html_url":"https://github.com/shootphp/shoot","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"purl":"pkg:github/shootphp/shoot","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shootphp%2Fshoot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shootphp%2Fshoot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shootphp%2Fshoot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shootphp%2Fshoot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/shootphp","download_url":"https://codeload.github.com/shootphp/shoot/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shootphp%2Fshoot/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279009079,"owners_count":26084549,"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-10-11T02:00:06.511Z","response_time":55,"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","php7","shoot","twig","twig-extension"],"created_at":"2025-01-29T06:31:36.726Z","updated_at":"2025-10-11T22:52:26.564Z","avatar_url":"https://github.com/shootphp.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Shoot\n[![License][ico-license]][link-license]\n[![Coverage][ico-coverage]][link-coverage]\n[![Code quality][ico-code-quality]][link-code-quality]\n\nShoot is an extension for [Twig][link-twig], a popular template engine for PHP. Shoot aims to make providing data to\nyour templates more manageable. Think of Shoot as a DI container for template data. \n\n## Prerequisites\nShoot assumes you're using PHP 7.2 and Twig to render templates in a [PSR-7][link-psr7] HTTP context. It also needs a\n[PSR-11][link-psr11] compatible DI container.\n\nAlthough not a requirement, a framework with support for [PSR-15][link-psr15] HTTP middleware does make your life a\nlittle easier.\n\n## What it does\nTypically, you first load your data and then use Twig to render that data into HTML. Shoot turns that around. You start\nrendering your templates and Shoot loads the data as needed. Enjoy this ASCII illustration:\n\n```\n+---------------+          +---------------+\n|    Request    |          |    Request    |\n+-------+-------+          +-------+-------+\n        |                          |     +---------+\n        |                          |     |         |\n+-------v-------+          +-------v-----v-+     +-+-------------+\n|   Load data   |          |  Render view  +-----\u003e   Load data   |\n+-------+-------+          +-------+-------+     +---------------+\n        |                          |\n        |                          |\n+-------v-------+          +-------v-------+\n|  Render view  |          |   Response    |\n+-------+-------+          +---------------+\n        |\n        |\n+-------v-------+\n|   Response    |\n+---------------+\n```\n\nFor this to work, Shoot introduces a few concepts:\n* _Presentation models_ – Think of them as data contracts for your templates, i.e. _Views_.\n* _Presenters_ – These do the actual work. A presenter is coupled to a specific presentation model, and loads just the\ndata it needs. These presenters are automatically invoked by Shoot as your templates are rendered.\n* _Middleware_ – As each template is rendered, it passes through Shoot's middleware pipeline. Invoking the presenters is\ndone by middleware, but there are plenty of other use cases, such as logging and debugging.\n\n## Installation\nShoot is available through [Packagist][link-packagist]. Simply install it with Composer:\n``` bash\n$ composer require shoot/shoot\n```\n\n## Getting started\nFirst, set up the pipeline. All views being rendered by Twig pass through it, and are processed by Shoot's middleware.\nFor Shoot to be useful, you'll need at least the `PresenterMiddleware`, which takes a DI container as its dependency.\n\nAll that's left is then to install Shoot in Twig:\n\n```php\n$middleware = [new PresenterMiddleware($container)];\n$pipeline = new Pipeline($middleware);\n$installer = new Installer($pipeline);\n\n$twig = $installer-\u003einstall($twig);\n```\n\nWith Shoot now set up, let's take a look at an example of how you can use it.\n\n### Request context\nBefore we're able to use Shoot's pipeline, it needs the current HTTP request being handled to provide context to its\nmiddleware and the presenters. You set it through the `withRequest` method, which accepts the request and a callback as\nits arguments. The callback is immediately executed and its result returned. During the execution of the callback, the\nrequest is available to the pipeline.\n\n```php\n$result = $pipeline-\u003ewithRequest($request, function () use ($twig): string {\n    return $twig-\u003erender('template.twig');\n});\n```\n\nIn the example above, `result` will contain the rendered HTML as returned by Twig.\n\nTo avoid having to manually set the request on the pipeline everywhere you render a template, it's recommended to handle\nthis in your HTTP middleware. This way, it's always taken care of. Shoot comes with PSR-15 compatible middleware to do\njust that: `Shoot\\Shoot\\Http\\ShootMiddleware`.\n\n### Presentation models\nNow with the plumbing out of the way, it's time to create our first presentation model. We'll use a blog post for our\nexample:\n\n```php\nnamespace Blog;\n\nfinal class Post extends PresentationModel implements HasPresenterInterface\n{\n    protected $author_name = '';\n    protected $author_url = '';\n    protected $body = '';\n    protected $title = '';\n\n    public function getPresenterName(): string\n    {\n        return PostPresenter::class;\n    }\n}\n```\n\nThe fields in a presentation model are the variables that'll be assigned to your template. That's why, as per Twig's\n[coding standards][link-twig-coding-standards], they use _snake_case_. These fields must be `protected`.\n\nThis presentation model implements the `HasPresenterInterface`. This indicates to Shoot that there's a presenter\navailable to load the data of this model. This interface defines the `getPresenterName` method. This method should\nreturn the name through which the presenter can be resolved by your DI container.\n\n### Templates\nTo assign the model to our template, we use Shoot's `model` tag. Set it at the top of your template and reference the\nclass name of your model:\n\n```twig\n{% model 'Blog\\\\Post' %}\n\u003c!doctype html\u003e\n\u003chtml\u003e\n    \u003chead\u003e\n        \u003ctitle\u003e{{ title }}\u003c/title\u003e\n    \u003c/head\u003e\n    \u003cbody\u003e\n        \u003ch1\u003e{{ title }}\u003c/h1\u003e\n        \u003cp\u003eWritten by \u003ca href=\"{{ author_url }}\"\u003e{{ author_name }}\u003c/a\u003e\u003c/p\u003e\n        \u003cp\u003e{{ body }}\u003c/p\u003e\n    \u003c/body\u003e\n\u003c/html\u003e\n```\n\n### Presenters\nWith the presentation model defined and assigned to the template, we can now focus on writing the presenter. Since\npresenters are retrieved from your DI container, you can easily inject any dependencies needed to load your data. In the\nfollowing example, we need a database and router:\n\n```php\nnamespace Blog;\n\nfinal class PostPresenter implements PresenterInterface\n{\n    private $database;\n    private $router;\n\n    public function __construct(PDO $database, Router $router)\n    {\n        $this-\u003edatabase = $database;\n        $this-\u003erouter = $router;\n    }\n\n    public function present(ServerRequestInterface $request, PresentationModel $presentationModel): PresentationModel\n    {\n        // post_id could be a variable in our route, e.g. /posts/{post_id}\n        $postId = $request-\u003egetAttribute('post_id', '');\n\n        $post = $this-\u003efetchPost($postId);\n\n        return $presentationModel-\u003ewithVariables([\n            'author_name' =\u003e $post['author_name'],\n            'author_url' =\u003e $this-\u003erouter-\u003epathFor('author', $post['author_id']),\n            'body' =\u003e $post['body'],\n            'title' =\u003e $post['title']\n        ]);\n    }\n\n    private function fetchPost(string $postId): array\n    {\n        // Fetches the post from the database\n    }\n}\n```\n\nWhenever the template is rendered, the presenter's `present` method will be called by Shoot with the current request\nand the presentation model assigned to the template.\n\nIt will fetch the necessary data from the database, look up the correct route to the author's profile and return the\npresentation model updated with its variables set. Shoot then assigns these variables to the template, and Twig takes\ncare of rendering it. Job done! \n\n## Changelog\nPlease see the [changelog][link-changelog] for more information on what has changed recently.\n\n## Testing\n``` bash\n$ composer run-script test\n```\n\n## License\nThe MIT License (MIT). Please see the [license file][link-license] for more information.\n\n[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square\n[ico-coverage]: https://img.shields.io/scrutinizer/coverage/g/shootphp/shoot.svg?style=flat-square\n[ico-code-quality]: https://img.shields.io/scrutinizer/g/shootphp/shoot.svg?style=flat-square\n[link-changelog]: CHANGELOG.md\n[link-coverage]: https://scrutinizer-ci.com/g/shootphp/shoot/code-structure\n[link-code-quality]: https://scrutinizer-ci.com/g/shootphp/shoot\n[link-license]: LICENSE.md\n[link-packagist]: https://packagist.org/packages/shoot/shoot\n[link-psr7]: https://www.php-fig.org/psr/psr-7/\n[link-psr11]: https://www.php-fig.org/psr/psr-11/\n[link-psr15]: https://www.php-fig.org/psr/psr-15/\n[link-twig]: https://twig.symfony.com/\n[link-twig-coding-standards]: https://twig.symfony.com/doc/2.x/coding_standards.html\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshootphp%2Fshoot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fshootphp%2Fshoot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshootphp%2Fshoot/lists"}