{"id":24912676,"url":"https://github.com/exteon/chaining-class-resolver","last_synced_at":"2025-03-28T04:36:13.252Z","repository":{"id":56980700,"uuid":"336010489","full_name":"exteon/chaining-class-resolver","owner":"exteon","description":"Weaving class loader framework for PHP modular plugins providing class chaining.","archived":false,"fork":false,"pushed_at":"2022-08-15T21:51:26.000Z","size":155,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-02-02T05:28:18.563Z","etag":null,"topics":["framework","modules","multiple-inheritance","php","plugins"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/exteon.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-02-04T16:15:33.000Z","updated_at":"2023-06-07T23:28:17.000Z","dependencies_parsed_at":"2022-08-21T11:50:36.239Z","dependency_job_id":null,"html_url":"https://github.com/exteon/chaining-class-resolver","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exteon%2Fchaining-class-resolver","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exteon%2Fchaining-class-resolver/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exteon%2Fchaining-class-resolver/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/exteon%2Fchaining-class-resolver/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/exteon","download_url":"https://codeload.github.com/exteon/chaining-class-resolver/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245972635,"owners_count":20702710,"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":["framework","modules","multiple-inheritance","php","plugins"],"created_at":"2025-02-02T05:28:21.230Z","updated_at":"2025-03-28T04:36:13.232Z","avatar_url":"https://github.com/exteon.png","language":"PHP","readme":"Together with \n[mapping-class-loader](https://github.com/exteon/mapping-class-loader),\nthis library provides a framework plugin development and loading. By using a\n\"chain-loading\" concept, it provides an implementation platform sitting \nsomewhere between mixins and multiple inheritance. \n\n## Abstract\n\nA common problem concerning a platform that offers plugins functionality is \nthis: we have a number of plugins that are developed against the same code-base\n(the same contracts), however they are unaware of each other (they must act \nlike black-boxes for other plugins).\n\nBy their nature, plugins are tight-coupled to the base code they extend or \ncustomize. At the same time, loaded plugins' effect needs to be additive. \nMultiple plugins coupled to the codebase must be aggregated to work together.\n\nSome of the solutions commonly implemented we found coming short:\n- decorators-based approach doesn't provide enough coupling, plugins \n  structure is an \"onion layer\" where output of an outer layer cannot be fed to\n  the input of an inner layer\n- observer pattern plugin systems (hooks, event-driven, generic invoker mixins \n  ect) are very costly to implement, maintain and debug\n  \nMost efficient plugin patterns rely on mixins, which are akin' to multiple \ninheritance. PHP's Traits, while a great feat, are not sufficient to\nimplement the mixin patterns needed for plugins, as they lack a static identity\n(static properties are duplicated to their implementing classes).\n\nOur solution provides a way of loading plugins in a chaining (layered) dynamic\nstructure (much like decorators), but uses source weaving to modify class \nhierarchy so that the resulting inheritanve model is fully-coupled.\n\nThe below illustrations will provide a more visual explanation of the chaining \nprocess. Let's start with how, starting from a codebase, plugin developers would\nadd specialisation by extending the existing classes, with the purpose that\ntheir specialisations will replace the initial implementation:\n\n![](doc/plugin%20development.svg)\n\nSo in the image above, both `Plugin1` and `Plugin2` extend the base code, while\n`Plugin3` is based on `Plugin2`.\n\nThe question is, how do we rejoin these structure so that we can use all 3 \nplugins in an app, with only one `A` class and one `B` class?\n\nIn comes `chaining-class-resolver`:\n\n![](doc/app%20development.svg)\n\nWith `chaining-class-loader`, code comes in modules, and all base code and \nplugins are modules that will be linearized. That is done at class load time, by\nchanging the inheritance. In the second diagram, you will have noticed how\n`\\Plugin2\\A` now inherits from `\\Plugin1\\A`, not from `\\Code\\Base\\A` as was in \nthe original source (first diagram).\n\nAll such linearized classes are then projected into a *Target namespace* so that\nan application can use i.e. `\\Target\\A` with relative obliviousness to the chain\nthat generated it.\n\nThis class weaving is done at runtime, so the process is transparent to the \napplication developer, that just needs to configure the modules. Thus, the \nplugins can be brought in from any source (such as, `composer`) and they won't\nneed to be customized in any way specific to the application, \n`chaining-class-resolver` will do all the magic.\n\n## Usage\n\nYou can find the example in the diagrams above implemented in the \n[example](example) directory. \n\nThe loader setup part is in [setup.inc.php](example/setup.inc.php)\n\nTo run the example, run [app.php](example/app.php)\n\nThe steps to set up chaining class loading are:\n\n### Creating the modules\n\nWe organize the modules in folders with a PSR-4 class structures:\n\n* [base](example/base) with namespace `Code\\Base`\n* [plugins/plugin1](example/plugins/plugin1) with namespace `Plugin1`\n* [plugins/plugin2](example/plugins/plugin1) with namespace `Plugin2`\n* [plugins/plugin3](example/plugins/plugin1) with namespace `Plugin3`\n\nEach module defines or extends classes `A` and/or `B` like in the \n[above diagram](doc/plugin%20development.svg)\n\nYou are free to use any directory structure, as long as you have a number of \nmodule directories each containing a PSR-4 namespace. \n\n### The chaining resolver\n\nWe get a resolver instance:\n\n```php\nuse Exteon\\Loader\\ChainingClassResolver\\ChainingClassResolver;\nuse Exteon\\Loader\\ChainingClassResolver\\Module;\nuse Exteon\\Loader\\ChainingClassResolver\\ClassFileResolver\\PSR4ClassFileResolver;\n\n\n$chainingClassResolver = new ChainingClassResolver(\n    [\n        new Module(\n            'Code base',\n            [new PSR4ClassFileResolver(__DIR__ . '/base', 'Code\\\\Base')]\n        ),\n        new Module(\n            'Plugin 1',\n            [new PSR4ClassFileResolver(__DIR__ . '/plugins/plugin1', 'Plugin1')]\n        ),\n        new Module(\n            'Plugin 2',\n            [new PSR4ClassFileResolver(__DIR__ . '/plugins/plugin2', 'Plugin2')]\n        ),\n        new Module(\n            'Plugin 3',\n            [new PSR4ClassFileResolver(__DIR__ . '/plugins/plugin3', 'Plugin3')]\n        )\n    ],\n    'Target'\n);\n```\n\nThe `$targetNs` constructor parameter defines the target namespace that the \nclass chain will be weaved into, in our case the classes will be chained under\n`\\Target`.\n\nModules will be chained in the order they are sent to the constructor; \nthis means, in the above example, `Plugin 1` will extend/override classes in \n`Code base`, `Plugin 3` will override all, ect.\n\n\n### Setting up the loader\n\nWe will use \n[exteon/mapping-class-loader](https://github.com/exteon/mapping-class-loader)\nto load the chained files:\n\n```php\nuse Exteon\\Loader\\MappingClassLoader\\MappingClassLoader;\nuse Exteon\\Loader\\MappingClassLoader\\StreamWrapLoader;\n\n$loader = new MappingClassLoader(\n    [],\n    [\n        $chainingClassResolver\n    ],\n    [],\n    new StreamWrapLoader([\n        'enableMapping' =\u003e true\n    ])\n);\n$loader-\u003eregister();\n```\n\nFor more details on using the `MappingClassLoader`, you can take a look at the\n[exteon/mapping-class-loader documentation](https://github.com/exteon/mapping-class-loader)\n.\n\n### Using the chained classes\n\nWe can now use the chained classes. All classes define or override the \n`whoami()` method, adding the result.\n\n```php\n    use Target\\A;\n    use Target\\B;\n\n    $a = new A();\n    $b = new B();\n\n    var_dump($a-\u003ewhoami());\n    var_dump($b-\u003ewhoami());\n```\n\nThe above code will produce the following result:\n\n```\narray(3) {\n  [0] =\u003e\n  string(11) \"Code\\Base\\A\"\n  [1] =\u003e\n  string(9) \"Plugin1\\A\"\n  [2] =\u003e\n  string(9) \"Plugin2\\A\"\n}\narray(2) {\n  [0] =\u003e\n  string(9) \"Plugin2\\B\"\n  [1] =\u003e\n  string(9) \"Plugin3\\B\"\n}\n```\n\nSo now the classes as chained as represented in \n[this diagram](doc/app%20development.svg).\n\n### Class hint files\n\nIf you open [app.php](example/app.php) in your smart IDE, the classes in the \n`Target` namespace will be unresolved and you will not have any autocompletion \nfor them. That is because `Target\\A` and `Target\\B` are not yet defined anywhere\nin the code.\n\nIn order to fix this, we need to create class hint files that will contain the\nstubs for the `Target` classes. We can do this by running \n[create-hints.php](example/create-hints.php) in the [example](example) \ndirectory.\n\nUsing the same [setup.inc.php](example/setup.inc.php), this tools runs\n\n```php\n$loader-\u003edumpHintClasses(__DIR__.'/dev/hints');\n```\n\nWhat this does is generate class hint files in `dev/hints`. These are PHP class\nfiles with PSR-4 structure that are generated in the directory you specify. Now\njust add the hints directory to your source directories in your IDE and now,\nyour `Target\\A` class will be defined as such:\n\n```php\nnamespace Target {\n    /**\n     * @extends \\Code\\Base\\A\n     * @extends \\Plugin1\\A\n     */\n    class A extends \\Plugin2\\A {}\n}\n```\n\n***Note*** \n\nThe `dumpHintClasses` tool should be ran whenever you add classes or change the\nclasses hierarchy in your project, reconfigure your modules ect. to regenerate \nthe hint files.\n\n***Note***\n\nThe hint classes contain only one extend, i.e. `Plugin2\\A`, which in your source\nfiles extends `Code\\Base\\A`. Therefore, if you now add a `someMethod()` method\non `Plugin1\\A`, this method will not be known on static analysis on `Target\\A`,\nthere will be no autocomplete for it, unless your IDE can read the multiple \n`@extends` annotations.\n\nIn a future version, we might address this by always using traits for class \nextending. (See [Traits chaining](#traits-chaining))\n\nHowever, the primary functionality of a plugin is to modify (i.e. override) \nexisting class methods. Therefore, we advise that when you think about extending\nan existing class's contract, you consider doing this using separate traits that \nyou `use` or you use object composition to keep added functionality into \nseparate classes. \n\n## Other features\n\n### Debugging\n\nEven though the `chaining-class-resolver` modifies (weaves) the source code to \nachieve its functionality, step debugging can be easily performed using \n`exteon/mapping-class-loader`'s mapping functionality. When debugging code, you\nwill be stepping on the original class files as usual. The only thing you need \nto do, as in the code examples above, is to set `'enableMapping' =\u003e true` to \n`StreamingWrapLoader`'s config.\n\nTo read more about the mapping functionality, please refer to the\n[exteon/mapping-class-loader documentation](https://github.com/exteon/mapping-class-loader#debug-mapping) \n.\n\n### \u003ca id=\"caching\"\u003e\u003c/a\u003eCaching\n\nThe class weaving process can be a significant overhead; \n[exteon/mapping-class-loader](https://github.com/exteon/mapping-class-loader)\nprovides caching for the weaved class files. To enable caching, you need to set\nthe `enableCaching` and `cacheDir` config options to the `MappingClassLoader`\nconstructor. For more details about caching, see the \n[exteon/mapping-class-loader documentation](https://github.com/exteon/mapping-class-loader#caching)\n.\n\nFor development, whenever you change the source classes, the cache needs to be \ncleared and reprimed; there is a work in progress for a change watcher over \n`inotify`, but until that is published, you should create your own tool, if you\nuse caching in a development setup.\n\n### \u003ca id=\"traits-chaining\"\u003e\u003c/a\u003eTraits chaining\n\nIn PHP, traits that implement the same method cannot be added on the same class\n([see here](https://www.php.net/manual/en/language.oop5.traits.php#language.oop5.traits.conflict)).\nWhile this is an artefact of the copy-paste trait implementation, it's also a\nsetback in traits reusability and expresiveness. Since `parent::` and `static::`\nare supported for traits and PHP doesn't support overloading, it makes sense \nthat multiple traits can override the same base method.\n\n`chaining-class-resolver` supports this by linearizing (chaining) the list of \ntraits in a class `uses` clause, creating intermediate classes, each using one \ntrait. Therefore, traits precedence is the order in which they are listed in the \n`use` clause.\n\nFor instance, this is possible with `chaining-class-resolver` (note the \ndifferent classes need to be implemented in different files as per PSR-4 \nstandard):\n\n```php\nclass A {\n    public function whoami(){\n        return ['A'];\n    }\n}\n```\n\n```php\ntrait T1 {\n    public function whoami(){\n        return array_merge(parent::whoami(),['T1']);\n    }\n}\n```\n\n```php\ntrait T2 {\n    public function whoami(){\n        return array_merge(parent::whoami(),['T2']);\n    }\n}\n```\n\n```php\nclass B extends A {\n    use T1,T2;\n    public function whoami(){\n        return array_merge(parent::whoami(),['B']);\n    }\n    \n}\n```\n\n```php\nvar_dump((new B())-\u003ewhoami());\n```\n\nThis will list `['A','T1','T2','B']` as class inheritance.\n\nPlease also further see [Multiple Inheritance Considerations](#multiple-inheritance-considerations) for a more \ndogmatic discussion of the implications. \n\n### \u003ca id=\"multiple-inheritance-considerations\"\u003e\u003c/a\u003eMultiple Inheritance Considerations\n\nIf chained classes, or multiple traits, all define a new method with the same \nname, the infamous \n[diamond problem](https://en.wikipedia.org/wiki/Multiple_inheritance#The_diamond_problem)\narises. `chaining-class-resolver` has no special handling for it, but then PHP\nalso doesn't (there is no syntax to explicitly signify an override). Therefore, \nhaving multiple traits defining the same new method is allowed, as long as, per \nPHP rules, the second method's signature is compatible with the first, making it \nan assumed override.\n\nIn the future we might implement a more stringent mechanism to detect and \naddress the diamond problem.\n\n### Chaining reflection\n\nYou can find information about the chained classes at runtime using \n`ChainedClassMeta`:\n\n```php\nChainedClassMeta::get('Some\\Class')-\u003eisChained()\n```\nReturns whether the named class is attached to a chain.\n\n```php\nChainedClassMeta::get('Some\\Class')-\u003egetChainParent()\n```\nReturns `Some\\Class`'s chain parent class as a `ChainedClassMeta` object or null\nif there is none. use `-\u003egetClassName()` to get this parent's class name.\n\n```php\nChainedClassMeta::get('Some\\Class')-\u003egetChainedClass()\n```\nReturns the `Some\\Class`'s chain target class (i.e. the end of the chain in the \ntarget namespace).\n\n```php\nChainedClassMeta::get('Some\\Class')-\u003egetModuleName()\n```\nReturns the name of the module that defines `Some\\Class`.\n\n```php\nChainedClassMeta::get('Some\\Class')-\u003egetChainTraits()\n```\nReturns the traits that were added in `Some\\Class`'s chain by all chained \nclasses. (But not the inherited ones).\n\n```php\nChainedClassMeta::get('Some\\Class')-\u003egetChainInterfaces()\n```\nReturns the interfaces that were added in `Some\\Class`'s chain by all chained \nclasses. (But not the inherited ones).\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexteon%2Fchaining-class-resolver","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fexteon%2Fchaining-class-resolver","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexteon%2Fchaining-class-resolver/lists"}