├── LICENSE.md ├── Makefile ├── README.md ├── composer.json ├── ecs.php ├── phpstan.neon └── src ├── Installer.php └── Plugin.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) nystudio107. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAJOR_VERSION?=5 2 | PLUGINDEV_PROJECT_DIR?=/Users/andrew/webdev/sites/plugindev/cms_v${MAJOR_VERSION}/ 3 | VENDOR?=nystudio107 4 | PROJECT_PATH?=${VENDOR}/$(shell basename $(CURDIR)) 5 | 6 | .PHONY: dev docs release 7 | 8 | # Start up the buildchain dev server 9 | dev: 10 | # Start up the docs dev server 11 | docs: 12 | # Run code quality tools, tests, and build the buildchain & docs in preparation for a release 13 | release: --code-quality --code-tests --buildchain-clean-build --docs-clean-build 14 | # The internal targets used by the dev & release targets 15 | --buildchain-clean-build: 16 | --code-quality: 17 | ${MAKE} -C ${PLUGINDEV_PROJECT_DIR} -- ecs check vendor/${PROJECT_PATH}/src --fix 18 | ${MAKE} -C ${PLUGINDEV_PROJECT_DIR} -- phpstan analyze -c vendor/${PROJECT_PATH}/phpstan.neon 19 | --code-tests: 20 | --docs-clean-build: 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/nystudio107/twig-bundle-installer/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/nystudio107/twig-bundle-installer/?branch=master) [![Build Status](https://scrutinizer-ci.com/g/nystudio107/twig-bundle-installer/badges/build.png?b=master)](https://scrutinizer-ci.com/g/nystudio107/twig-bundle-installer/build-status/master) [![Code Intelligence Status](https://scrutinizer-ci.com/g/nystudio107/twig-bundle-installer/badges/code-intelligence.svg?b=master)](https://scrutinizer-ci.com/code-intelligence) 2 | 3 | # Twig Bundle Installer plugin for Composer 4 | 5 | A Composer plugin that installs & manages Twig Bundles in your `templates/vendor/` directory 6 | 7 | ![Screenshot](./resources/twig-bundle.png) 8 | 9 | ## Overview 10 | 11 | Twig Bundle Installer is a Composer installer that installs and manages Twig Bundles in your `templates/` directory. It introduces the concept of Twig _bundles_ and installs them in `templates/vendor/`, similarly to how you’d normally install PHP packages in `vendor/`. (And it doesn’t change anything about that; your PHP bundles will still live in `vendor`.) 12 | 13 | It implements a new Composer package type named `twig-bundle`, which should be used when publishing Twig Bundles. Composer manages it all, and Twig Bundle Installer is not tied to any particular CMS or system. Anything that uses Twig might find it useful. 14 | 15 | This allows you to install and update Twig templates across multiple projects in a managed way. 16 | 17 | ## Why Though? 18 | 19 | - Stop copying useful bits of Twig between projects; include them easily and keep them *all* up to date. 20 | - Share useful Twig components with other developers and teams in whatever projects you want. 21 | - Reuse these same bits anywhere you use Twig; Craft CMS, Drupal, Grav, Symfony... even Laravel, Statamic, and beyond. 22 | - Improve your documentation once your base components all live together in one place. 23 | - Distribute your Craft plugin’s sample templates in a more convenient, flexible, and versionable way. 24 | - Utilize *template* dependencies as easily as PHP packages. 25 | 26 | ## Why Twig Bundle Installer? 27 | 28 | I'd originally thought of the idea implemented in Twig Bundle Installer when working on re-usable Twig components. 29 | 30 | Later the idea came up again when I worked on a base Twig templating layer as discussed in the [An Effective Base Twig Templating Setup](https://nystudio107.com/blog/an-effective-twig-base-templating-setup) article. 31 | 32 | Then it idea came up _again_ when discussing with a colleague how they managed multiple brand properties in a large [Craft CMS](https://craftcms.com) install via separate plugins. Each brand site had its own custom plugin which was mostly a wrapper for the templates needed for said site. 33 | 34 | So if something comes up 3x or more, I think it's probably worth trying out… 35 | 36 | ## Using Twig Bundle Installer 37 | 38 | ### Consuming Twig Bundles 39 | 40 | #### Adding Twig Bundles to your Project 41 | 42 | To use Twig Bundles in your own project, first you need to add Twig Bundle Installer to your project's `composer.json`: 43 | 44 | ```json 45 | { 46 | "require": { 47 | "nystudio107/twig-bundle-installer": "^1.0.0" 48 | } 49 | } 50 | ``` 51 | 52 | Because Twig Bundle Installer is a Composer plugin, you will also need to tell Composer that's it okay to use this plugin via [`config:allow-plugins`](https://getcomposer.org/doc/06-config.md#allow-plugins) in your `composer.json` file: 53 | 54 | ```json 55 | { 56 | "config": { 57 | "allow-plugins": { 58 | "nystudio107/twig-bundle-installer": true 59 | } 60 | } 61 | } 62 | ``` 63 | 64 | Then you can add in the vendor/package name of the Twig Bundle you want to use just like you would any Composer package: 65 | 66 | ```json 67 | { 68 | "require": { 69 | "nystudio107/twig-bundle-installer": "^1.0.0", 70 | "nystudio107/test-twig-bundle": "^1.0.0" 71 | } 72 | } 73 | 74 | ``` 75 | 76 | Then just do a: 77 | ``` 78 | composer install 79 | ``` 80 | 81 | What Twig Bundle Installer does is for Composer packages that are of the type `twig-bundle` instead of putting them in the `vendor/` directory, it will put them in the `templates/vendor/` directory. 82 | 83 | In the above example, you'll end up with something like this: 84 | ``` 85 | ❯ tree -L 4 templates/vendor 86 | templates/vendor 87 | └── nystudio107 88 | └── test-twig-bundle 89 | ├── CHANGELOG.md 90 | ├── composer.json 91 | ├── LICENSE.md 92 | ├── README.md 93 | └── templates 94 | ├── fizz-buzz.twig 95 | ├── elementary-my-dear-watson.twig 96 | └── five-minute-read.twig 97 | 98 | 3 directories, 8 files 99 | ``` 100 | 101 | This means that you can install & update these Twig Bundles across multiple projects. They can be Twig Bundles you've created, or Twig Bundles others have created. 102 | 103 | It works just like any Composer package does, because Twig Bundle Installer is just a layer on top of Composer that routes packages of the type `twig-bundle` to a different directory. 104 | 105 | Commands you're used to such as `composer require`, `composer update`, etc. all work as you'd expect. 106 | 107 | Example including a template from a Twig Bundle: 108 | 109 | ```twig 110 | {% include 'vendor/nystudio107/test-twig-bundle/templates/fizz-buzz.twig' %} 111 | ``` 112 | 113 | #### Twig Bundle Considerations 114 | 115 | Since Twig Bundle Installer is looking for a directory in your project root named `templates/` that points to your Twig templates directory: 116 | 117 | * You should treat the `templates/vendor/` directory as **read only** just like you do the `vendor/` directory 118 | * If you store your templates somewhere else, you can create a symlink or alias from `templates/` to where you store your templates, or you can set the `TEMPLATES_VENDOR_DIR` environment variable (either in the environment itself, or in your `.env` file in your project root if you're using [Dotenv](https://github.com/vlucas/phpdotenv)) 119 | * If you exclude your `vendor/` directory from your Git repo, you probably would want to add `templates/vendor/` to your `.gitignore` as well. Twig Bundle Installer will automatically put `.gitignore` file in the `templates/vendor/` directory for you 120 | 121 | Example `.gitignore` file: 122 | ``` 123 | /vendor 124 | /templates/vendor 125 | ``` 126 | 127 | #### Local Repositories 128 | 129 | If you want to use local Twig Bundles while you work on them, you can do that via the [Composer Repositories](https://getcomposer.org/doc/05-repositories.md) setting: 130 | 131 | ```json 132 | { 133 | "require": { 134 | "nystudio107/bundle-twig-installer": "^1.0.0", 135 | "nystudio107/twig-test-bundle": "^1.0.0" 136 | }, 137 | "repositories": [ 138 | { 139 | "type": "path", 140 | "url": "../../twig/*", 141 | "options": { 142 | "symlink": true 143 | } 144 | } 145 | ] 146 | } 147 | 148 | ``` 149 | 150 | Where the `url` setting is a path to where your source Twig Bundles live. 151 | 152 | 153 | ### Creating Twig Bundles 154 | 155 | To create a Twig Bundle, create a directory with a `Composer.json` file in it that looks like this: 156 | ```json 157 | { 158 | "name": "nystudio107/test-twig-bundle", 159 | "description": "Test bundle of Twig templates for Bundle Installer", 160 | "version": "1.0.0", 161 | "keywords": [ 162 | "twig", 163 | "twig-bundle", 164 | "composer", 165 | "installer", 166 | "bundle" 167 | ], 168 | "type": "twig-bundle", 169 | "license": "MIT", 170 | "minimum-stability": "stable" 171 | } 172 | ``` 173 | 174 | ...but obviously change the `name` to your `vendor/bundle` name, and fill in your own description, etc. The key is that you must have the `type` set to `twig-bundle`: 175 | 176 | ``` 177 | "type": "twig-bundle", 178 | ``` 179 | 180 | You'll then want to publish this to a [GitHub](https://github.com/) or other Git repo, publish it on [Packagist.org](https://packagist.org/) so others can install it via Composer 181 | 182 | If you've never published a package on Packagist before, just follow the instructions on [Packagist.org](https://packagist.org/) or read the [Packagist and the PHP ecosystem](https://www.bugsnag.com/blog/packagist-and-the-php-ecosystem) article. 183 | 184 | You can use the [Test Twig Bundle](https://github.com/nystudio107/test-twig-bundle) as an example to follow. 185 | 186 | ## Twig Bundle Installer Roadmap 187 | 188 | This project is usable as-is, but it's also very much in the germination phase. I'm curious to see what uses people find for it, or potentially none at all. 189 | 190 | Some ideas: 191 | 192 | * Bundles could include CSS and JavaScript that the installer builds a `manifest.json` for something else to consume 193 | * Framework specific tools could compliment Twig Bundle Installer by automatically publishing bundles on the frontend 194 | * Technically, the technique described here would work fine for [Antlers](https://docs.statamic.com/antlers) or [Blade](https://laravel.com/docs/5.8/blade) and other templating systems as well. 195 | 196 | Brought to you by [nystudio107](https://nystudio107.com/) 197 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nystudio107/twig-bundle-installer", 3 | "description": "Install, update, and manage Twig template bundles via Composer", 4 | "version": "1.1.2", 5 | "keywords": [ 6 | "twig", 7 | "composer", 8 | "installer", 9 | "bundle", 10 | "craftcms", 11 | "drupal", 12 | "symphony" 13 | ], 14 | "homepage": "https://nystudio107.com", 15 | "type": "composer-plugin", 16 | "support": { 17 | "email": "info@nystudio107.com", 18 | "issues": "https://github.com/nystudio107/twig-bundle-installer/issues", 19 | "source": "https://github.com/nystudio107/twig-bundle-installer", 20 | "docs": "https://github.com/nystudio107/twig-bundle-installer" 21 | }, 22 | "license": "MIT", 23 | "minimum-stability": "stable", 24 | "require": { 25 | "php": ">=5.4", 26 | "composer-plugin-api": "^1.0 || ^2.0" 27 | }, 28 | "require-dev": { 29 | "composer/composer": "^1.0 || ^2.0", 30 | "craftcms/ecs": "dev-main", 31 | "phpstan/phpstan": "^1.4.6" 32 | }, 33 | "scripts": { 34 | "phpstan": "phpstan --ansi --memory-limit=1G", 35 | "check-cs": "ecs check --ansi", 36 | "fix-cs": "ecs check --fix --ansi" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "nystudio107\\composer\\": "src/" 41 | } 42 | }, 43 | "extra": { 44 | "class": "nystudio107\\composer\\Plugin" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | paths([ 8 | __DIR__ . '/src', 9 | __FILE__, 10 | ]); 11 | $ecsConfig->parallel(); 12 | $ecsConfig->sets([SetList::CRAFT_CMS_4]); 13 | }; 14 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 5 3 | paths: 4 | - src 5 | -------------------------------------------------------------------------------- /src/Installer.php: -------------------------------------------------------------------------------- 1 | safeLoad(); 51 | } 52 | $this->vendorDir = rtrim(getenv('TEMPLATES_VENDOR_DIR') ?: self::TEMPLATES_VENDOR_DIR, '/'); 53 | $this->type = self::TWIG_BUNDLE_PACKAGE_TYPE; 54 | } 55 | 56 | /** 57 | * @inheritdoc 58 | */ 59 | public function supports($packageType): bool 60 | { 61 | return $packageType === self::TWIG_BUNDLE_PACKAGE_TYPE; 62 | } 63 | 64 | /** 65 | * @inheritdoc 66 | */ 67 | public function install(InstalledRepositoryInterface $repo, PackageInterface $package) 68 | { 69 | // Install the twig bundle like a normal Composer package 70 | $promise = parent::install($repo, $package); 71 | // Write out the .gitignore file 72 | $this->writeGitIgnore(); 73 | // Composer v2 might return a promise 74 | if ($promise instanceof PromiseInterface) { 75 | return $promise; 76 | } 77 | return null; 78 | } 79 | 80 | /** 81 | * @inheritdoc 82 | */ 83 | public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) 84 | { 85 | // Install the twig bundle like a normal Composer package 86 | $promise = parent::update($repo, $initial, $target); 87 | // Write out the .gitignore file 88 | $this->writeGitIgnore(); 89 | // Composer v2 might return a promise 90 | if ($promise instanceof PromiseInterface) { 91 | return $promise; 92 | } 93 | return null; 94 | } 95 | 96 | // Protected Methods 97 | // ========================================================================= 98 | 99 | /** 100 | * Write out a .gitignore file in the TEMPLATES_VENDOR_DIR if it doesn't exist already 101 | * 102 | * @return void 103 | */ 104 | protected function writeGitIgnore(): void 105 | { 106 | // Create the directory if it doesn't exist 107 | $dir = self::TEMPLATES_VENDOR_DIR; 108 | if (!file_exists($dir)) { 109 | if (!mkdir($dir, 0777, true) && !is_dir($dir)) { 110 | throw new RuntimeException(sprintf('Directory "%s" was not created', $dir)); 111 | } 112 | } 113 | // Create the .gitignore file if it doesn't exist 114 | $file = self::TEMPLATES_VENDOR_DIR . '/.gitignore'; 115 | if (!file_exists($file)) { 116 | file_put_contents($file, "*\n!.gitignore\n"); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Plugin.php: -------------------------------------------------------------------------------- 1 | installer = new Installer($io, $composer, Installer::TWIG_BUNDLE_PACKAGE_TYPE); 44 | $composer->getInstallationManager()->addInstaller($this->installer); 45 | } 46 | 47 | /** 48 | * @inheritdoc 49 | */ 50 | public function deactivate(Composer $composer, IOInterface $io) 51 | { 52 | $composer->getInstallationManager()->removeInstaller($this->installer); 53 | } 54 | 55 | /** 56 | * @inheritdoc 57 | */ 58 | public function uninstall(Composer $composer, IOInterface $io) 59 | { 60 | } 61 | } 62 | --------------------------------------------------------------------------------