{"id":24030259,"url":"https://github.com/getpop/component-model-configuration","last_synced_at":"2025-07-25T20:40:39.928Z","repository":{"id":37458408,"uuid":"198031606","full_name":"getpop/component-model-configuration","owner":"getpop","description":"[READ ONLY] Adds the configuration layer to the component model","archived":false,"fork":false,"pushed_at":"2023-09-07T09:25:18.000Z","size":202,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-08T17:33:28.914Z","etag":null,"topics":["component","component-model","configuration","model","pop"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/getpop.png","metadata":{"files":{"readme":"README.md","changelog":null,"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":"2019-07-21T08:24:21.000Z","updated_at":"2022-01-07T13:05:43.000Z","dependencies_parsed_at":"2025-01-08T17:31:26.434Z","dependency_job_id":"335f67bf-b301-4e5b-b3c7-8f9a98dee774","html_url":"https://github.com/getpop/component-model-configuration","commit_stats":{"total_commits":304,"total_committers":2,"mean_commits":152.0,"dds":"0.17763157894736847","last_synced_commit":"7e24a2f2ed33cb646f7d3aac954654ace6da515f"},"previous_names":[],"tags_count":39,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/getpop%2Fcomponent-model-configuration","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/getpop%2Fcomponent-model-configuration/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/getpop%2Fcomponent-model-configuration/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/getpop%2Fcomponent-model-configuration/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/getpop","download_url":"https://codeload.github.com/getpop/component-model-configuration/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240788674,"owners_count":19857691,"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":["component","component-model","configuration","model","pop"],"created_at":"2025-01-08T17:31:07.881Z","updated_at":"2025-02-26T03:35:33.331Z","avatar_url":"https://github.com/getpop.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Component Model Configuration\n\n\u003c!--\n[![Build Status][ico-travis]][link-travis]\n[![Quality Score][ico-code-quality]][link-code-quality]\n[![Software License][ico-license]](LICENSE.md)\n[![Latest Version on Packagist][ico-version]][link-packagist]\n[![Coverage Status][ico-scrutinizer]][link-scrutinizer]\n[![Total Downloads][ico-downloads]][link-downloads]\n--\u003e\n\nAdds the configuration level to the component hierarchy, through which the data API can be extended into an application\n\n## Install\n\nVia Composer\n\n``` bash\ncomposer require getpop/component-model-configuration\n```\n\n## Development\n\nThe source code is hosted on the [GatoGraphQL monorepo](https://github.com/GatoGraphQL/GatoGraphQL), under [`SiteBuilder/packages/component-model-configuration`](https://github.com/GatoGraphQL/GatoGraphQL/tree/master/layers/SiteBuilder/packages/component-model-configuration).\n\n## Usage\n\nInitialize the component:\n\n``` php\n\\PoP\\Root\\App::stockAndInitializeModuleClasses([([\n    \\PoP\\ConfigurationComponentModel\\Module::class,\n]);\n```\n\n## Architecture Design and Implementation\n\n### Configuration\n\nConfiguration values are added under functions:\n\n- `function getImmutableConfiguration($component, \u0026$props)`\n- `function getMutableonmodelConfiguration($component, \u0026$props)`\n- `function getMutableonrequestConfiguration($component, \u0026$props)`\n\nFor instance:\n\n```php\n// Implement the components properties ...\nfunction getImmutableConfiguration($component, \u0026$props) \n{\n  $ret = parent::getImmutableConfiguration($component, $props);\n\n  switch ($component-\u003ename) {\n    case self::COMPONENT_SOMENAME:\n      $ret['description'] = __('Some description');\n      $ret['classes']['description'] = 'jumbotron';\n      break;\n  }\n\n  return $ret;\n}\n```\n\nPlease notice that the configuration receives the `$props` parameter, hence it can print configuration values set through props. `immutable` and `mutable on model` configuration values are initialized through `initModelProps`, and `mutable on request` ones are initialized through `initRequestProps`:\n\n```php\n// Implement the components properties ...\nfunction getImmutableConfiguration($component, \u0026$props) \n{\n  $ret = parent::getImmutableConfiguration($component, $props);\n\n  switch ($component-\u003ename) {\n    case self::COMPONENT_SOMENAME:\n      $ret['showmore'] = $this-\u003egetProp($component, $props, 'showmore');\n      $ret['class'] = $this-\u003egetProp($component, $props, 'class');\n      break;\n  }\n\n  return $ret;\n}\n\nfunction initModelProps($component, \u0026$props) \n{\n  switch ($component-\u003ename) {\n    case self::COMPONENT_SOMENAME:      \n      $this-\u003esetProp($component, $props, 'showmore', false);\n      $this-\u003eappendProp($component, $props, 'class', 'text-center');\n      break;\n  }\n\n  parent::initModelProps($component, $props);\n}\n```\n\n### Client-Side Caching\n\nTo cache the configuration for all components in the client, and keep them available to be reused, we simply deep merge all the responses together. For instance, if the first request brings this response:\n\n```javascript\n{\n  \"component1\": {\n    configuration: {\n      class: \"topcomponent\"\n    },\n    components: {\n      \"component2\": {\n        configuration: {\n          class: \"some-class\"\n        }\n      }\n    }\n  }\n}\n```\n\nAnd the second response brings this response:\n\n```javascript\n{\n  \"component3\": {\n    configuration: {\n      class: \"topcomponent\"\n    },\n    components: {\n      \"component4\": {\n        configuration: {\n          class: \"another-class\"\n        }\n      }\n    }\n  }\n}\n```\n\nThen deep merging the responses together will result in:\n\n```javascript\n{\n  \"component1\": {\n    configuration: {\n      class: \"topcomponent\"\n    },\n    components: {\n      \"component2\": {\n        configuration: {\n          class: \"some-class\"\n        }\n      }\n    }\n  },\n  \"component3\": {\n    configuration: {\n      class: \"topcomponent\"\n    },\n    components: {\n      \"component4\": {\n        configuration: {\n          class: \"another-class\"\n        }\n      }\n    }\n  }\n}\n```\n\nAnd we can perfectly reuse the configuration starting from \"component1\" to reprint the first request, and the configuration starting from \"component3\" to reprint the second request.\n\nThat was easy, however from now on it gets more complicated. What happens if the component's descendants are not static, but can change depending on the context, such as the requested URL or other inputs? For instance, we could have a component \"single-post\" which changes its descendant component based on the post type of the requested object, choosing between components \"layout-post\" or \"layout-event\", so that the component hierarchy alternates from this:\n\n```javascript\n\"single-post\"\n  components\n    \"layout-post\"\n```\n\nto this:\n\n```javascript\n\"single-post\"\n  components\n    \"layout-event\"\n```\n\nSimilarly, even within the same component hierarchy, a component could have a property value change for different URLs. For instance, a component \"post-layout\" can have a property \"class\" with value \"post-{id}\", where \"{id}\" is the id of the requested post, so that we can add styles for specific posts such as `.post-37 { background-color: red; }` and `.post-224 { background-color: green; }`. Then, posts with ids 37 and 224, even though they have the same component hierarchy, their configurations will alternate from this:\n\n```javascript\n\"single-post\"\n  components\n    \"layout-post\"\n      configuration\n        class: \"post-37\"\n```\n\nto this:\n\n```javascript\n\"single-post\"\n  components\n    \"layout-post\"\n      configuration\n        class: \"post-224\"\n```\n\nLet's explore what happens in these two situations described above when deep merging the results. In the first case, for instance, if the first request brings this response:\n\n```javascript\n{\n  \"component1\": {\n    configuration: {\n      class: \"topcomponent\"\n    },\n    components: {\n      \"component2\": {\n        configuration: {\n          class: \"some-class\"\n        }\n      }\n    }\n  }\n}\n```\n\nAnd the second response brings this response:\n\n```javascript\n{\n  \"component1\": {\n    configuration: {\n      class: \"topcomponent\"\n    },\n    components: {\n      \"component3\": {\n        configuration: {\n          class: \"another-class\"\n        }\n      }\n    }\n  }\n}\n```\n\nThen deep merging the responses together will result in:\n\n```javascript\n{\n  \"component1\": {\n    configuration: {\n      class: \"topcomponent\"\n    },\n    components: {\n      \"component2\": {\n        configuration: {\n          class: \"some-class\"\n        }\n      },\n      \"component3\": {\n        configuration: {\n          class: \"another-class\"\n        }\n      }\n    }\n  }\n}\n```\n\nAs it can be seen, after merging the response from the second request, the data that was the same (`class: \"topcomponent\"`) didn't affect the merged object, and the new information was appended to the existing object but without overriding any data. However, originally the first response has \"component1\" with only \"component2\" as a descendant, but after the merge, \"component1\" has two descendants, \"component2\" and \"component3\". Then, if loading again the URL for the first response and reusing the cached configuration, below \"component1\" it will print \"component2\" and \"component3\" instead of only \"component2\" as it should be.\n\nTo address this issue, the configuration can add a property \"descendants\" explicitly declaring which are its subcomponents, to know which components must be rendered and ignore the rest, even though their data is still part of the merged JSON object. Then, the first and second response will look like this:\n\n```javascript\n{\n  \"component1\": {\n    configuration: {\n      class: \"topcomponent\",\n      descendants: [\"component2\"]\n    },\n    components: {\n      \"component2\": {\n        configuration: {\n          class: \"some-class\"\n        }\n      }\n    }\n  }\n}\n\n{\n  \"component1\": {\n    configuration: {\n      class: \"topcomponent\",\n      descendants: [\"component3\"]\n    },\n    components: {\n      \"component3\": {\n        configuration: {\n          class: \"another-class\"\n        }\n      }\n    }\n  }\n}\n```\n\nAnd the merged configuration will look like this:\n\n```javascript\n{\n  \"component1\": {\n    configuration: {\n      class: \"topcomponent\",\n      descendants: [\"component3\"]\n    },\n    components: {\n      \"component2\": {\n        configuration: {\n          class: \"some-class\"\n        }\n      },\n      \"component3\": {\n        configuration: {\n          class: \"another-class\"\n        }\n      }\n    }\n  }\n}\n```\n\nBut now, the value for property \"descendants\" in the cached object has been overriden with the value from the second response, bringing us to the second issue stated earlier on about differing property values. Then, if loading again the URL for the first response and reusing the cached configuration, below \"component1\" it will print \"component3\" instead of \"component2\" as it should be.\n\nThe issue about differing properties arises from the fact that configuration values are set not only according to the component hierarchy, but also to the requested URL. For instance, the following component hierarchy:\n\n```javascript\n\"single-post\"\n  components\n    \"layout-post\"\n```\n\nCan produce the following two different configuration outputs:\n\n```javascript\n\"single-post\"\n  components\n    \"layout-post\"\n      configuration\n        class: \"post-37\"\n```\n\nand \n\n```javascript\n\"single-post\"\n  components\n    \"layout-post\"\n      configuration\n        class: \"post-224\"\n```\n\nThe solution is to deep merge the configurations from different requests without overriding differing properties, either at the component hierarchy or URL levels, is to have the configuration split into 3 separate subsections: \"immutable\", \"mutableonmodel\" (where \"model\" is equivalent to \"component hierarchy\") and \"mutableonrequest\". Every property in the configuration must be placed under exactly 1 of the 3 sections, like this:\n\n- **immutable:** Contains properties which never change, such as `class: \"topcomponent\"`\n- **mutableonmodel:** Contains properties which can change based on the component hierarchy, such as `descendants: [\"component2\"]`\n- **mutableonrequest:** Contains properties which can change based on the requested URL, such as `class: \"post-37\"`\n\nFollowing this scheme, a first request may produce the following response:\n\n```javascript\n{\n  immutable: {\n    \"single-post\": {\n      configuration: {\n        class: \"topcomponent\"\n      }\n    }\n  },\n  mutableonmodel: {\n    \"single-post\": {\n      configuration: {\n        descendants: [\"layout-post\"]\n      }\n    }\n  },\n  mutableonrequest: {\n    \"single-post\": {\n      components: {\n        \"layout-post\": {\n          configuration: {\n            class: \"post-37\"\n          }\n        }\n      }\n    }\n  }\n}\n```\n\nAs it can be observed, because the properties from the 3 sections do not overlap, then merging the 3 sections for the request produces the whole configuration once again:\n\n```javascript\n{\n  \"single-post\": {\n    configuration: {\n      class: \"topcomponent\",\n      descendants: [\"layout-post\"]\n    },\n    components: {\n      \"layout-post\": {\n        configuration: {\n          class: \"post-37\"\n        }\n      }\n    }\n  }\n}\n```\n\nNext, the cache in the client is kept in 3 separate objects, one for each of the subsections, with sections \"mutableonmodel\" and \"mutableonrequest\" storing their data under appropriate keys: \"mutableonmodel\" under a key called \"modelInstanceId\", which represents a hash of the component hierarchy, and \"mutableonrequest\" under the requested URL. The request above will then be cached like this (assuming a \"modelInstanceId\" with value \"bwKtq*8H\" and URL \"/posts/some-post/\"):\n\n```javascript\nimmutable =\u003e \n  {\n    \"single-post\": {\n      configuration: {\n        class: \"topcomponent\"\n      }\n    }\n  }\n\nmutableonmodel =\u003e \n  {\n    \"bwKtq*8H\": {\n      \"single-post\": {\n        configuration: {\n          descendants: [\"layout-post\"]\n        }\n      }\n    }\n  }\n\nmutableonrequest =\u003e \n  {\n    \"/posts/some-post/\": {\n      \"single-post\": {\n        components: {\n          \"layout-post\": {\n            configuration: {\n              class: \"post-37\"\n            }\n          }\n        }\n      }\n    }\n  }\n```\n\nIf then we obtain the response for a second request, the cache is updated like this (assuming a \"modelInstanceId\" with value \"6C7Lu$\\3\" and URL \"/posts/some-event/\"):\n\n```javascript\nimmutable =\u003e \n  {\n    \"single-post\": {\n      configuration: {\n        class: \"topcomponent\"\n      }\n    }\n  }\n\nmutableonmodel =\u003e \n  {\n    \"bwKtq*8H\": {\n      \"single-post\": {\n        configuration: {\n          descendants: [\"layout-post\"]\n        }\n      }\n    },\n    \"6C7Lu$\\3\": {\n      \"single-post\": {\n        configuration: {\n          descendants: [\"layout-event\"]\n        }\n      }\n    }\n  }\n\nmutableonrequest =\u003e \n  {\n    \"/posts/some-post/\": {\n      \"single-post\": {\n        components: {\n          \"layout-post\": {\n            configuration: {\n              class: \"post-37\"\n            }\n          }\n        }\n      }\n    },\n    \"/posts/some-event/\": {\n      \"single-post\": {\n        components: {\n          \"layout-event\": {\n            configuration: {\n              class: \"post-45\"\n            }\n          }\n        }\n      }\n    }\n  }\n```\n\nAs it can be observed, \"immutable\" holds the common parts of the structure, while \"mutableonmodel\" and \"mutableonrequest\" hold the deltas. Hence, this scheme identifies common data and stores it only once, and all dissimilar entries are stored and accessible on their own. If most of the configuration doesn't change within the component hierarchy, then the information stored under \"immutable\" will make the bulk of the stored information, succeeding in minimizing the amount of data that is cached.\n\nFinally, given the \"modelInstanceId\" and URL for any request we can obtain the 3 separate branches from the 3 sections, and merge them all together to recreate the whole configuration from the cache. \n\nThe merging can be done in the server-side too: If there is no need to cache the configuration on the client, then we can avoid the added complexity of dealing with the three subsections by adding parameter `dataoutputmode=combined` to the URL.\n\n## PHP versions\n\nRequirements:\n\n- PHP 8.1+ for development\n- PHP 7.2+ for production\n\n### Supported PHP features\n\nCheck the list of [Supported PHP features in `GatoGraphQL/GatoGraphQL`](https://github.com/GatoGraphQL/GatoGraphQL/blob/master/docs/supported-php-features.md)\n\n### Preview downgrade to PHP 7.2\n\nVia [Rector](https://github.com/rectorphp/rector) (dry-run mode):\n\n```bash\ncomposer preview-code-downgrade\n```\n\n## Standards\n\n[PSR-1](https://www.php-fig.org/psr/psr-1), [PSR-4](https://www.php-fig.org/psr/psr-4) and [PSR-12](https://www.php-fig.org/psr/psr-12).\n\nTo check the coding standards via [PHP CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer), run:\n\n``` bash\ncomposer check-style\n```\n\nTo automatically fix issues, run:\n\n``` bash\ncomposer fix-style\n```\n\n## Change log\n\nPlease see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.\n\n## Testing\n\nTo execute [PHPUnit](https://phpunit.de/), run:\n\n``` bash\ncomposer test\n```\n\n## Static Analysis\n\nTo execute [PHPStan](https://github.com/phpstan/phpstan), run:\n\n``` bash\ncomposer analyse\n```\n\n## Report issues\n\nTo report a bug or request a new feature please do it on the [GatoGraphQL monorepo issue tracker](https://github.com/GatoGraphQL/GatoGraphQL/issues).\n\n## Contributing\n\nWe welcome contributions for this package on the [GatoGraphQL monorepo](https://github.com/GatoGraphQL/GatoGraphQL) (where the source code for this package is hosted).\n\nPlease see [CONTRIBUTING](CONTRIBUTING.md) and [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) for details.\n\n## Security\n\nIf you discover any security related issues, please email leo@getpop.org instead of using the issue tracker.\n\n## Credits\n\n- [Leonardo Losoviz][link-author]\n- [All Contributors][link-contributors]\n\n## License\n\nGNU General Public License v2 (or later). Please see [License File](LICENSE.md) for more information.\n\n[ico-version]: https://img.shields.io/packagist/v/getpop/component-model-configuration.svg?style=flat-square\n[ico-license]: https://img.shields.io/badge/license-GPLv2-brightgreen.svg?style=flat-square\n[ico-travis]: https://img.shields.io/travis/getpop/component-model-configuration/master.svg?style=flat-square\n[ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/getpop/component-model-configuration.svg?style=flat-square\n[ico-code-quality]: https://img.shields.io/scrutinizer/g/getpop/component-model-configuration.svg?style=flat-square\n[ico-downloads]: https://img.shields.io/packagist/dt/getpop/component-model-configuration.svg?style=flat-square\n\n[link-packagist]: https://packagist.org/packages/getpop/component-model-configuration\n[link-travis]: https://travis-ci.org/getpop/component-model-configuration\n[link-scrutinizer]: https://scrutinizer-ci.com/g/getpop/component-model-configuration/code-structure\n[link-code-quality]: https://scrutinizer-ci.com/g/getpop/component-model-configuration\n[link-downloads]: https://packagist.org/packages/getpop/component-model-configuration\n[link-author]: https://github.com/leoloso\n[link-contributors]: ../../../../../../contributors\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgetpop%2Fcomponent-model-configuration","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgetpop%2Fcomponent-model-configuration","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgetpop%2Fcomponent-model-configuration/lists"}