{"id":18550288,"url":"https://github.com/xp-forge/frontend","last_synced_at":"2025-04-09T22:31:14.659Z","repository":{"id":48796177,"uuid":"127759856","full_name":"xp-forge/frontend","owner":"xp-forge","description":"Web frontends","archived":false,"fork":false,"pushed_at":"2024-12-21T12:02:22.000Z","size":335,"stargazers_count":1,"open_issues_count":2,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-24T12:39:42.919Z","etag":null,"topics":["annotations","assets","async","brotli","bundler","caching","compression","csrf-tokens","fingerprinting","frontend","gzip","immutable","php7","php8","template-engine","web","xp-framework"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/xp-forge.png","metadata":{"files":{"readme":"README.md","changelog":"ChangeLog.md","contributing":null,"funding":null,"license":null,"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":"2018-04-02T13:27:59.000Z","updated_at":"2024-12-21T12:01:48.000Z","dependencies_parsed_at":"2023-01-29T23:01:10.858Z","dependency_job_id":"c6351a62-db3a-46a1-94c3-b2cc6d151f68","html_url":"https://github.com/xp-forge/frontend","commit_stats":{"total_commits":331,"total_committers":2,"mean_commits":165.5,"dds":"0.0030211480362537513","last_synced_commit":"93512f54f442ff76ccb55547b2048a340609ee29"},"previous_names":[],"tags_count":48,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xp-forge%2Ffrontend","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xp-forge%2Ffrontend/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xp-forge%2Ffrontend/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xp-forge%2Ffrontend/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/xp-forge","download_url":"https://codeload.github.com/xp-forge/frontend/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247999859,"owners_count":21031046,"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":["annotations","assets","async","brotli","bundler","caching","compression","csrf-tokens","fingerprinting","frontend","gzip","immutable","php7","php8","template-engine","web","xp-framework"],"created_at":"2024-11-06T21:04:07.172Z","updated_at":"2025-04-09T22:31:14.645Z","avatar_url":"https://github.com/xp-forge.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"Web frontends\n=============\n\n[![Build status on GitHub](https://github.com/xp-forge/frontend/workflows/Tests/badge.svg)](https://github.com/xp-forge/frontend/actions)\n[![XP Framework Module](https://raw.githubusercontent.com/xp-framework/web/master/static/xp-framework-badge.png)](https://github.com/xp-framework/core)\n[![BSD Licence](https://raw.githubusercontent.com/xp-framework/web/master/static/licence-bsd.png)](https://github.com/xp-framework/core/blob/master/LICENCE.md)\n[![Requires PHP 7.0+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-7_0plus.svg)](http://php.net/)\n[![Supports PHP 8.0+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-8_0plus.svg)](http://php.net/)\n[![Latest Stable Version](https://poser.pugx.org/xp-forge/frontend/version.svg)](https://packagist.org/packages/xp-forge/frontend)\n\nFrontends based on `xp-forge/web`, using annotation-based routing.\n\n## Example\n\nFrontend uses handler classes with methods annotated with HTTP verbs to handle routing. These methods return a context, which is passed along with the template name to the template engine.\n\n```php\nuse web\\frontend\\{Handler, Get, Param};\n\n#[Handler]\nclass Hello {\n\n  #[Get]\n  public function greet(#[Param('name')] $param) {\n    return ['name' =\u003e $param ?: 'World'];\n  }\n}\n```\n*Note: For PHP 7, the `Param` annotation must be on a line by itself, [see here](https://gist.github.com/thekid/8ce84b0d0de8fce5b6dd5faa22e1d716#file-home-class-php)!*\n\nFor the above class, the template engine will receive *home* as template name and the returned map as context. This library contains only the skeleton for templating - the [xp-forge/handlebars-templates](https://github.com/xp-forge/handlebars-templates) library implements it. For the rest of the examples, we'll be using it.\n\nThe handlebars template *hello.handlebars* (calculated from the lowercase version of the above handler class' name) is quite straight-forward:\n\n```handlebars\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003chead\u003e\n  \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\"\u003e\n  \u003ctitle\u003eHello World\u003c/title\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n  \u003ch1\u003eHello {{name}}\u003c/h1\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\nFinally, wiring it together is done in the application class, as follows:\n\n```php\nuse web\\Application;\nuse web\\frontend\\{AssetsFrom, Frontend, Handlebars};\n\nclass Site extends Application {\n\n  /** @return [:var] */\n  public function routes() {\n    $assets= new AssetsFrom($this-\u003eenvironment-\u003epath('src/main/webapp'));\n    $templates= new Handlebars($this-\u003eenvironment-\u003epath('src/main/handlebars'));\n\n    return [\n      '/favicon.ico' =\u003e $assets,\n      '/static'      =\u003e $assets,\n      '/'            =\u003e new Frontend(new Hello(), $templates)\n    ];\n  }\n}\n```\n\nTo run it, use `xp -supervise web Site`, which will serve the site at http://localhost:8080/.\n\n## Organizing your code\n\nIn real-life situations, you will not want to put all of your code into the `Hello` class. In order to separate code out into various classes, place all handler classes inside a dedicated package:\n\n```bash\n@FileSystemCL\u003c./src/main/php\u003e\npackage org.example.web {\n\n  public class org.example.web.Home\n  public class org.example.web.User\n  public class org.example.web.Group\n}\n```\n\nThen use the delegation API provided by the `HandlersIn` class:\n\n```php\nuse web\\frontend\\{Frontend, HandlersIn};\n\n// ...inside the routes() method, as seen above:\nnew Frontend(new HandlersIn('org.example.web'), $templates);\n```\n\n## Handling routes and methods\n\nThe `Handler` annotation can include a path which is used as a prefix for all method routes in a handler class. Placeholders can be used to select method parameters from the request URI.\n\n```php\nuse web\\frontend\\{Handler, Get};\n\n#[Handler('/hello')]\nclass Hello {\n\n  #[Get]\n  public function world() {\n    return ['greet' =\u003e 'World'];\n  }\n\n  #[Get('/{name}')]\n  public function person(string $name) {\n    return ['greet' =\u003e $name];\n  }\n}\n```\n\nThe above method routes will only accept `GET` requests. `POST` request methods can be annotated with `Post`, `PUT` with `Put`, and so on. To overwrite the method used for POST requests, pass the special `_method` field:\n\n```html\n\u003cform action=\"/example\" method=\"POST\"\u003e\n  \u003cinput type=\"hidden\" name=\"_method\" value=\"PUT\"\u003e\n  \u003c!-- Rest of form --\u003e\n\u003c/form\u003e\n```\n\nThis will route the request as if it had been issued as `PUT /example HTTP/1.1`.\n\n### Views \n\nRoute methods can return `web.frontend.View` instances to have more control over the response:\n\n```php\nuse web\\frontend\\View;\n\n// Equivalent of the above world() method's return value\nreturn View::named('hello')-\u003ewith(['greet' =\u003e 'World']);\n\n// Redirecting to either paths or absolute URIs\nreturn View::redirect('/hello/World');\n\n// No content\nreturn View::empty()-\u003estatus(204);\n\n// Add headers and caching, here: for 7 days\nreturn View::named('blog')\n  -\u003ewith($article)\n  -\u003eheader('X-Binford', '6100 (more power)')\n  -\u003emodified($modified)\n  -\u003ecache('max-age=604800, must-revalidate')\n;\n```\n\n## Serving assets\n\nAssets are delivered by the `AssetsFrom` handler as seen above. It takes care of content types, handling conditional and range requests for partial content, as well as compression.\n\n### Sources\n\nThe constructor accepts single paths as well as an array of paths which will be searched for the requested asset. The first path to provide the asset is selected, the file being served from there.\n\n```php\nuse web\\frontend\\AssetsFrom;\n\n// Single source\n$assets= new AssetsFrom($this-\u003eenvironment-\u003epath('src/main/webapp'));\n\n// Multiple sources\n$assets= new AssetsFrom([\n  $this-\u003eenvironment-\u003epath('src/main/webapp'),\n  $this-\u003eenvironment-\u003epath('vendor/example/layout-lib/src/main/webapp'),\n]);\n```\n\n### Caching\n\nAssets can be delivered with a `Cache-Control` header by passing it to the `with` function. In this example, assets are cached for 28 days, but clients are asked to revalidate using conditional requests before using their cached copy.\n\n```php\nuse web\\frontend\\AssetsFrom;\n\n$assets= (new AssetsFrom($path))-\u003ewith([\n  'Cache-Control' =\u003e 'max-age=2419200, must-revalidate'\n]);\n```\n\n### Compression\n\nAssets can also be delivered in compressed forms to save bandwidth. The typical bundled JavaScript library can be megabytes in raw size! By using e.g. Brotli, this can be drastically reduced to a couple of hundred kilobytes.\n\n* The request URI is mapped to the asset file name\n* If the clients sends an `Accept-Encoding` header, it is parsed and the client preference negotiated\n* The server tries *[file]*.br (for Brotli), *[file]*.bz2 (for BZip2), *[file]*.gz (for GZip) and *[file]*.dfl (for Deflate), and only sends the uncompressed version if none exists nor is acceptable.\n\n*Note: Assets are not compressed on the fly as this would cause unnecessary server load.*\n\n### Asset fingerprinting\n\nGenerated assets can be fingerprinted by embedding a version identifier in the filename, e.g. *[file].[version].[ext]*. Every time their contents change, the version (or *fingerprint*) changes, and with it the filename. These assets can then be regarded \"immutable\", and served with an \"infinite\" maximum age. Bundlers (like Webpack or the one built-in to this library) will create an *asset manifest* along with these assets.\n\n```php\nuse web\\frontend\\{AssetsFrom, AssetsManifest};\n\n$manifest= new AssetsManifest($path-\u003eresolve('manifest.json'));\n$assets= new AssetsFrom($path)-\u003ewith(fn($uri) =\u003e [\n  'Cache-Control' =\u003e $manifest-\u003eimmutable($uri) ?? 'max-age=2419200, must-revalidate'\n]);\n```\n\nBecause mapping the filenames happens in the template engine, the manifest must also be passed there:\n\n```php\nuse web\\frontend\\Handlebars;\nuse web\\frontend\\helpers\\Assets;\n\n$templates= new Handlebars($path, [new Assets($manifest)]);\n```\n\nThe handlebars code then uses the *asset* helper to lookup the filename including the fingerprint:\n\n```handlebars\n\u003clink href=\"/static/{{asset 'vendor.css'}}\" rel=\"stylesheet\"\u003e\n```\n\n*This way, we don't have to commit changes to our handlebars file every time the assets are changed, which may happen often!*\n\n### The built-in bundler\n\nBundling assets makes sense from a security standpoint, but also to reduce HTTP requests. This library comes with a `bundle` subcommand, which can generate JavaScript and CSS bundles from dependencies tracked in `package.json`.\n\n```json\n{\n  \"dependencies\": {\n    \"simplemde\": \"^1.11\",\n    \"transliteration\": \"^2.1\"\n  },\n  \"bundles\": {\n    \"vendor\": {\n      \"simplemde\": \"dist/simplemde.min.js | dist/simplemde.min.css\",\n      \"transliteration\": \"dist/browser/bundle.umd.min.js\"\n    }\n  }\n}\n```\n\nTo create the bundles to the *src/main/webapp/static* directory and the assets manifest, run the following:\n\n```bash\n$ xp bundle -m src/main/webapp/manifest.json src/main/webapp/static\n# ...\n```\n\nThis will create *vendor.[fingerprint].js* and *vendor.[fingerprint].css* files as well as compressed versions (*if the zlib and [brotli](https://github.com/kjdev/php-ext-brotli) PHP extensions are available*) and the assets manifest, which maps the file names without fingerprints to those with.\n\nThe bundler can also resolve local files, URLs as well as [Google fonts](https://fonts.google.com/):\n\n```json\n{\n  \"bundles\": {\n    \"vendor\": {\n      \"src/main/js\": \"index.js\",\n      \"https://cdn.amcharts.com/lib/4\": \"core.js | charts.js | themes/kelly.js\",\n      \"fonts://display=swap\": \"Overpass\"\n    }\n  }\n}\n```\n\n## Error handling\n\nBy default, errors and exceptions will yield in a minimalistic error page with the corresponding error code (*defaulting to 500 Internal Server Error*) shown. Exceptions can be handled by a closure, a status code or by default, and decide to return a view of their own. This view is loaded from the *errors/* subfolder and passed a context of `['cause' =\u003e $exception]`.\n\n```php\nuse web\\frontend\\{HandlersIn, Frontend, Exceptions};\nuse org\\example\\{InvalidOrder, LinkExpired};\nuse lang\\Throwable;\n\n$frontend= (new Frontend(new HandlersIn('org.example.web'), $templates))\n  -\u003ehandling((new Exceptions())\n    -\u003ecatch(InvalidOrder::class, fn($e) =\u003e View::error(503, 'invalid-order')),\n    -\u003ecatch(LinkExpired::class, 404) // uses template \"errors/404\"\n    -\u003ecatch(Throwable::class)        // catch-all, errors/{status} for web.Error, errors/500 for others\n  )\n;\n```\n\nUsing our handlebars engine from above, the template *errors/404.handlebars* could look like this:\n\n```handlebars\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003chead\u003e\n  \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\"\u003e\n  \u003ctitle\u003eError 404\u003c/title\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n  \u003ch1\u003eNot found\u003c/h1\u003e\n  \u003cp\u003e{{cause.message}}\u003c/p\u003e\n\n  {{! Log errors !}}\n  {{log request.uri \"~\" cause level=\"error\"}}\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n## Security\n\nThis library sets the following security header defaults:\n\n* `X-Content-Type-Options: nosniff` - prevents browsers from [MIME sniffing](https://mimesniff.spec.whatwg.org/)\n* `X-Frame-Options: DENY` - prevents site from being embedded in an `\u003ciframe\u003e`.\n* `Referrer-Policy: no-referrer-when-downgrade` - doesn't send HTTP referrer over unencrypted connections.\n\nTo configure framing, referrer and content security policies, use the *security()* fluent interface:\n\n```php\nuse web\\frontend\\{Frontend, Security};\n\n$frontend= (new Frontend($delegates, $templates))\n  -\u003eenacting((new Security())\n    -\u003eframing('SAMEORIGIN')\n    -\u003ereferrers('strict-origin')\n    -\u003ecsp([\n      'default-src' =\u003e '\"none\"',\n      'script-src'  =\u003e ['\"self\"', '\"nonce-{{nonce}}\"', 'https://example.com'],\n      // etcetera\n    ])\n  )\n;\n```\n\nRead more about hardening response headers at https://scotthelme.co.uk/hardening-your-http-response-headers/ or watch this talk: https://www.youtube.com/watch?v=mr230uotw-Y\n\n## Performance\n\nWhen using the production servers, the application's code is only compiled and its setup only runs once. This gives us lightning-fast response times:\n\n![Network console screenshot](https://user-images.githubusercontent.com/696742/114273532-adc30b00-9a1a-11eb-9267-e0ceda8d64e2.png)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fxp-forge%2Ffrontend","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fxp-forge%2Ffrontend","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fxp-forge%2Ffrontend/lists"}