├── roave-bc-check.yaml ├── src ├── Config │ ├── Routes.php │ ├── Filters.php │ └── Registrar.php ├── Entities │ └── Theme.php ├── Views │ ├── select.php │ └── form.php ├── Database │ ├── Seeds │ │ └── ThemeSeeder.php │ └── Migrations │ │ └── 20190620100802_create_table_themes.php ├── Helpers │ └── themes_helper.php ├── Commands │ ├── ThemesList.php │ └── ThemesAdd.php ├── Models │ └── ThemeModel.php ├── Controllers │ └── Themes.php ├── ThemeBundle.php └── Filters │ └── ThemesFilter.php ├── infection.json.dist ├── .php-cs-fixer.dist.php ├── psalm_autoload.php ├── psalm.xml ├── composer-unused.php ├── UPGRADING.md ├── LICENSE ├── SECURITY.md ├── composer.json ├── deptrac.yaml ├── rector.php └── README.md /roave-bc-check.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - '#\[BC\] SKIPPED: .+ could not be found in the located source#' 4 | -------------------------------------------------------------------------------- /src/Config/Routes.php: -------------------------------------------------------------------------------- 1 | post('themes/select', '\Tatter\Themes\Controllers\Themes::select'); 8 | -------------------------------------------------------------------------------- /src/Config/Filters.php: -------------------------------------------------------------------------------- 1 | aliases['themes'] = ThemesFilter::class; 12 | -------------------------------------------------------------------------------- /src/Entities/Theme.php: -------------------------------------------------------------------------------- 1 | 'bool', 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src/" 5 | ], 6 | "excludes": [ 7 | "Config", 8 | "Database/Migrations", 9 | "Views" 10 | ] 11 | }, 12 | "logs": { 13 | "text": "build/infection.log" 14 | }, 15 | "mutators": { 16 | "@default": true 17 | }, 18 | "bootstrap": "vendor/codeigniter4/framework/system/Test/bootstrap.php" 19 | } 20 | -------------------------------------------------------------------------------- /src/Config/Registrar.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | public static function Preferences(): array 13 | { 14 | return [ 15 | 'theme' => 'Default', 16 | ]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Views/select.php: -------------------------------------------------------------------------------- 1 | 2 | 11 | -------------------------------------------------------------------------------- /src/Views/form.php: -------------------------------------------------------------------------------- 1 | name; 6 | } 7 | 8 | $data = [ 9 | 'selected' => $selected, 10 | 'auto' => true, 11 | ]; 12 | ?> 13 | 14 |
15 | 16 | 17 | 18 |
19 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | files() 9 | ->in([ 10 | __DIR__ . '/src/', 11 | __DIR__ . '/tests/', 12 | ]) 13 | ->exclude('build') 14 | ->append([__FILE__]); 15 | 16 | $overrides = []; 17 | 18 | $options = [ 19 | 'finder' => $finder, 20 | 'cacheFile' => 'build/.php-cs-fixer.cache', 21 | ]; 22 | 23 | return Factory::create(new CodeIgniter4(), $overrides, $options)->forProjects(); 24 | -------------------------------------------------------------------------------- /psalm_autoload.php: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Database/Seeds/ThemeSeeder.php: -------------------------------------------------------------------------------- 1 | first()) { 18 | $themes->insert([ 19 | 'name' => 'Default', 20 | 'path' => 'themes/default', 21 | 'description' => 'Default theme', 22 | ]); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /composer-unused.php: -------------------------------------------------------------------------------- 1 | $config 11 | ->setAdditionalFilesFor('codeigniter4/framework', [ 12 | ...Glob::glob(__DIR__ . '/vendor/codeigniter4/framework/system/Helpers/*.php'), 13 | ]) 14 | ->setAdditionalFilesFor('tatter/preferences', [ 15 | __DIR__ . '/vendor/tatter/preferences/src/Helpers/preferences_helper.php', 16 | ]); 17 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrade Guide 2 | 3 | ## Version 1 to 2 4 | *** 5 | 6 | > Note: This is a complete refactor! Please be sure to read the docs carefully before upgrading. 7 | 8 | * Uses `Tatter\Assets` for handling its files, so the root folder for theme paths is now `config('Assets')->directory` (default: `FCPATH . 'assets/'`) 9 | * Uses `Tatter\Preferences` for managing persistent settings which strongly recommends `codeigniter4/authentication-implementation` 10 | * Asset injection is now handled by the `ThemesFilter`; remove any references to the tag views and read the docs to set up the filter 11 | * Theme settings now track based on the theme name instead of its ID so pay attention to naming 12 | -------------------------------------------------------------------------------- /src/Helpers/themes_helper.php: -------------------------------------------------------------------------------- 1 | where('name', $name)->first()) { 22 | return $theme; 23 | } 24 | 25 | throw new RuntimeException('Unable to locate the theme: ' . $name); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Commands/ThemesList.php: -------------------------------------------------------------------------------- 1 | table('themes')->select('name, path, description, dark, created_at') 20 | ->where('deleted_at IS NULL') 21 | ->orderBy('name', 'asc') 22 | ->get()->getResultArray(); 23 | 24 | if (empty($rows)) { 25 | CLI::write('No available themes.', 'yellow'); 26 | } else { 27 | $thead = ['Name', 'Path', 'Description', 'Dark?', 'Created']; 28 | CLI::table($rows, $thead); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tatter Software 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 | -------------------------------------------------------------------------------- /src/Models/ThemeModel.php: -------------------------------------------------------------------------------- 1 | 'required|max_length[255]', 25 | 'path' => 'required|max_length[255]', 26 | ]; 27 | 28 | /** 29 | * Faked data for Fabricator. 30 | */ 31 | public function fake(Generator &$faker): Theme 32 | { 33 | return new Theme([ 34 | 'name' => $faker->catchPhrase, 35 | 'path' => 'themes/' . $faker->word, 36 | 'description' => $faker->sentence, 37 | 'dark' => random_int(0, 1), 38 | ]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Database/Migrations/20190620100802_create_table_themes.php: -------------------------------------------------------------------------------- 1 | ['type' => 'varchar', 'constraint' => 255], 13 | 'path' => ['type' => 'varchar', 'constraint' => 255], 14 | 'description' => ['type' => 'text', 'null' => true], 15 | 'dark' => ['type' => 'boolean', 'default' => 0], 16 | 'created_at' => ['type' => 'datetime', 'null' => true], 17 | 'updated_at' => ['type' => 'datetime', 'null' => true], 18 | 'deleted_at' => ['type' => 'datetime', 'null' => true], 19 | ]; 20 | 21 | $this->forge->addField('id'); 22 | $this->forge->addField($fields); 23 | 24 | $this->forge->addKey('name'); 25 | $this->forge->addKey('created_at'); 26 | 27 | $this->forge->createTable('themes'); 28 | } 29 | 30 | public function down() 31 | { 32 | $this->forge->dropTable('themes'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Controllers/Themes.php: -------------------------------------------------------------------------------- 1 | request->getVar('theme')) { 20 | return redirect() 21 | ->back() 22 | ->withInput() 23 | ->with('errors', ['No theme selected.']); 24 | } 25 | 26 | // Look up the theme 27 | if (! $theme = model(ThemeModel::class)->where('name', $name)->first()) { 28 | return redirect() 29 | ->back() 30 | ->withInput() 31 | ->with('errors', ['Could not find theme: ' . $name . '.']); 32 | } 33 | 34 | // Update the setting and send back 35 | preference('theme', $theme->name); 36 | 37 | return redirect() 38 | ->back() 39 | ->with('success', 'User theme changed to ' . $theme->name . '.'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | The development team and community take all security issues seriously. **Please do not make public any uncovered flaws.** 4 | 5 | ## Reporting a Vulnerability 6 | 7 | Thank you for improving the security of our code! Any assistance in removing security flaws will be acknowledged. 8 | 9 | **Please report security flaws by emailing the development team directly: support@tattersoftware.com**. 10 | 11 | The lead maintainer will acknowledge your email within 48 hours, and will send a more detailed response within 48 hours indicating 12 | the next steps in handling your report. After the initial reply to your report, the security team will endeavor to keep you informed of the 13 | progress towards a fix and full announcement, and may ask for additional information or guidance. 14 | 15 | ## Disclosure Policy 16 | 17 | When the security team receives a security bug report, they will assign it to a primary handler. 18 | This person will coordinate the fix and release process, involving the following steps: 19 | 20 | - Confirm the problem and determine the affected versions. 21 | - Audit code to find any potential similar problems. 22 | - Prepare fixes for all releases still under maintenance. These fixes will be released as fast as possible. 23 | 24 | ## Comments on this Policy 25 | 26 | If you have suggestions on how this process could be improved please submit a Pull Request. 27 | -------------------------------------------------------------------------------- /src/ThemeBundle.php: -------------------------------------------------------------------------------- 1 | useCache) { 23 | // Use the hash of these items for the cache key 24 | $key = 'assets-theme' . $theme->id; 25 | 26 | // If there's a cached version then return it 27 | if ($bundle = cache($key)) { 28 | return $bundle; 29 | } 30 | } 31 | 32 | $bundle = new self(); 33 | 34 | // Resolve the directory for the active theme 35 | $root = rtrim(Asset::config()->directory, '\\/ '); 36 | $directory = $root . DIRECTORY_SEPARATOR . trim($theme->path, '/ '); 37 | 38 | if (! is_dir($directory)) { 39 | throw new UnexpectedValueException('Theme directory does not exist: ' . $directory); 40 | } 41 | 42 | // Locate all CSS and JSS files in the them path 43 | $files = (new FileCollection()) 44 | ->addDirectory($directory) 45 | ->retainPattern('#(.*)\.css$|(.*)\.js$#i'); // limit to .css and .js files 46 | 47 | // Create an Asset from each relative path and add it to the Bundle 48 | foreach ($files as $file) { 49 | $relativePath = str_replace($root, '', $file); 50 | $bundle->add(Asset::createFromPath($relativePath)); 51 | } 52 | 53 | if (isset($key)) { 54 | cache()->save($key, $bundle); 55 | } 56 | 57 | return $bundle; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tatter/themes", 3 | "description": "Lightweight theme manager for CodeIgniter 4", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "codeigniter", 8 | "codeigniter4", 9 | "themes", 10 | "css" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Matthew Gatner", 15 | "email": "mgatner@tattersoftware.com", 16 | "homepage": "https://tattersoftware.com", 17 | "role": "Developer" 18 | } 19 | ], 20 | "homepage": "https://github.com/tattersoftware/codeigniter4-themes", 21 | "require": { 22 | "php": "^7.4 || ^8.0", 23 | "tatter/assets": "^3.0", 24 | "tatter/preferences": "^1.0" 25 | }, 26 | "require-dev": { 27 | "codeigniter4/framework": "^4.1", 28 | "tatter/imposter": "^1.0", 29 | "tatter/tools": "^2.0" 30 | }, 31 | "minimum-stability": "dev", 32 | "prefer-stable": true, 33 | "autoload": { 34 | "psr-4": { 35 | "Tatter\\Themes\\": "src" 36 | }, 37 | "exclude-from-classmap": [ 38 | "**/Database/Migrations/**" 39 | ] 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Tests\\Support\\": "tests/_support" 44 | } 45 | }, 46 | "config": { 47 | "allow-plugins": { 48 | "phpstan/extension-installer": true 49 | } 50 | }, 51 | "scripts": { 52 | "analyze": [ 53 | "phpstan analyze", 54 | "psalm", 55 | "rector process --dry-run" 56 | ], 57 | "ci": [ 58 | "Composer\\Config::disableProcessTimeout", 59 | "@deduplicate", 60 | "@analyze", 61 | "@composer normalize --dry-run", 62 | "@test", 63 | "@inspect", 64 | "@style" 65 | ], 66 | "deduplicate": "phpcpd app/ src/", 67 | "inspect": "deptrac analyze --cache-file=build/deptrac.cache", 68 | "mutate": "infection --threads=2 --skip-initial-tests --coverage=build/phpunit", 69 | "retool": "retool", 70 | "style": "php-cs-fixer fix --verbose --ansi --using-cache=no", 71 | "test": "phpunit" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Commands/ThemesAdd.php: -------------------------------------------------------------------------------- 1 | "The name of the theme (e.g. 'Dark Night')", 19 | 'path' => "The relative path of the theme (e.g. 'themes/dark')", 20 | 'description' => "A brief description of the theme (e.g. 'A stormy theme fitting for night')", 21 | 'dark' => 'Whether this theme uses dark colors (y/n)', 22 | ]; 23 | 24 | public function run(array $params = []) 25 | { 26 | $config = Asset::config(); 27 | 28 | // Consume or prompt for a theme name 29 | if (! $name = array_shift($params)) { 30 | $name = CLI::prompt('Name of the theme', null, 'required'); 31 | } 32 | 33 | // Consume or prompt for the path 34 | if (! $path = array_shift($params)) { 35 | $path = CLI::prompt('Path to the theme, relative to ' . $config->directory, null, 'required'); 36 | } 37 | 38 | // Verify theme path 39 | if (! is_dir($config->directory . $path)) { 40 | CLI::write('Warning! Directory not found: ' . $config->directory . $path, 'yellow'); 41 | CLI::write('Be sure to add the directory and files before using the theme', 'yellow'); 42 | } 43 | 44 | // Consume or prompt for description 45 | if (! $description = array_shift($params)) { 46 | $description = CLI::prompt('Description'); 47 | } 48 | 49 | // Consume or prompt for dark status 50 | if (! $dark = array_shift($params)) { 51 | $dark = CLI::prompt('Dark theme?', ['n', 'y']); 52 | } 53 | 54 | // Try to create the record 55 | $result = model(ThemeModel::class)->save([ 56 | 'name' => $name, 57 | 'path' => $path, 58 | 'description' => $description, 59 | 'dark' => ($dark === 'y'), 60 | ]); 61 | 62 | if (! $result) { 63 | $this->showError(new RuntimeException(implode(' ', model(ThemeModel::class)->errors()))); 64 | 65 | return; 66 | } 67 | 68 | $this->call('themes:list'); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Filters/ThemesFilter.php: -------------------------------------------------------------------------------- 1 | getBody())) { 39 | return null; 40 | } 41 | 42 | // Check CLI separately for coverage 43 | if (is_cli() && ENVIRONMENT !== 'testing') { 44 | return null; // @codeCoverageIgnore 45 | } 46 | 47 | // Only run on HTML content 48 | if (strpos($response->getHeaderLine('Content-Type'), 'html') === false) { 49 | return null; 50 | } 51 | 52 | // If no theme was provided then load the current 53 | if (empty($arguments)) { 54 | helper(['preferences', 'settings', 'themes']); 55 | $themes = [theme()]; 56 | } 57 | // Otherwise look them up by name 58 | else { 59 | $themes = []; 60 | 61 | foreach ($arguments as $name) { 62 | if ($theme = model(ThemeModel::class)->where('name', $name)->first()) { 63 | $themes[] = $theme; 64 | } else { 65 | throw new RuntimeException('Unable to locate the theme: ' . $name); 66 | } 67 | } 68 | } 69 | 70 | // Build the tag blocks 71 | $headTags = []; 72 | $bodyTags = []; 73 | 74 | foreach ($themes as $theme) { 75 | $bundle = ThemeBundle::createFromTheme($theme); 76 | 77 | $headTags[] = $bundle->head(); 78 | $bodyTags[] = $bundle->body(); 79 | } 80 | 81 | $headTags = implode(PHP_EOL, $headTags); 82 | $bodyTags = implode(PHP_EOL, $bodyTags); 83 | 84 | // Short circuit? 85 | // @phpstan-ignore-next-line https://github.com/phpstan/phpstan/issues/7117 86 | if ($headTags === '' && $bodyTags === '') { 87 | return null; 88 | } 89 | 90 | $body = $response->getBody(); 91 | 92 | // Add any head content right before the closing head tag 93 | if ($headTags !== '') { 94 | $body = str_replace('', $headTags . PHP_EOL . '', $body); 95 | } 96 | // Add any body content right before the closing body tag 97 | if ($bodyTags !== '') { 98 | $body = str_replace('', $bodyTags . PHP_EOL . '', $body); 99 | } 100 | 101 | // Use the new body and return the updated Response 102 | return $response->setBody($body); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /deptrac.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | paths: 3 | - ./src/ 4 | - ./vendor/codeigniter4/framework/system/ 5 | exclude_files: 6 | - '#.*test.*#i' 7 | layers: 8 | - name: Model 9 | collectors: 10 | - type: bool 11 | must: 12 | - type: className 13 | regex: .*[A-Za-z]+Model$ 14 | must_not: 15 | - type: directory 16 | regex: vendor/.* 17 | - name: Vendor Model 18 | collectors: 19 | - type: bool 20 | must: 21 | - type: className 22 | regex: .*[A-Za-z]+Model$ 23 | - type: directory 24 | regex: vendor/.* 25 | - name: Controller 26 | collectors: 27 | - type: bool 28 | must: 29 | - type: className 30 | regex: .*\/Controllers\/.* 31 | must_not: 32 | - type: directory 33 | regex: vendor/.* 34 | - name: Vendor Controller 35 | collectors: 36 | - type: bool 37 | must: 38 | - type: className 39 | regex: .*\/Controllers\/.* 40 | - type: directory 41 | regex: vendor/.* 42 | - name: Config 43 | collectors: 44 | - type: bool 45 | must: 46 | - type: directory 47 | regex: src/Config/.* 48 | must_not: 49 | - type: className 50 | regex: .*Services 51 | - type: directory 52 | regex: vendor/.* 53 | - name: Vendor Config 54 | collectors: 55 | - type: bool 56 | must: 57 | - type: directory 58 | regex: vendor/.*/Config/.* 59 | must_not: 60 | - type: className 61 | regex: .*Services 62 | - name: Entity 63 | collectors: 64 | - type: bool 65 | must: 66 | - type: directory 67 | regex: src/Entities/.* 68 | must_not: 69 | - type: directory 70 | regex: vendor/.* 71 | - name: Vendor Entity 72 | collectors: 73 | - type: bool 74 | must: 75 | - type: directory 76 | regex: vendor/.*/Entities/.* 77 | - name: View 78 | collectors: 79 | - type: bool 80 | must: 81 | - type: directory 82 | regex: src/Views/.* 83 | must_not: 84 | - type: directory 85 | regex: vendor/.* 86 | - name: Vendor View 87 | collectors: 88 | - type: bool 89 | must: 90 | - type: directory 91 | regex: vendor/.*/Views/.* 92 | - name: Service 93 | collectors: 94 | - type: className 95 | regex: .*Services.* 96 | ruleset: 97 | Entity: 98 | - Config 99 | - Model 100 | - Service 101 | - Vendor Config 102 | - Vendor Entity 103 | - Vendor Model 104 | Config: 105 | - Service 106 | - Vendor Config 107 | Model: 108 | - Config 109 | - Entity 110 | - Service 111 | - Vendor Config 112 | - Vendor Entity 113 | - Vendor Model 114 | Service: 115 | - Config 116 | - Vendor Config 117 | 118 | # Ignore anything in the Vendor layers 119 | Vendor Model: 120 | - Config 121 | - Service 122 | - Vendor Config 123 | - Vendor Controller 124 | - Vendor Entity 125 | - Vendor Model 126 | - Vendor View 127 | Vendor Controller: 128 | - Service 129 | - Vendor Config 130 | - Vendor Controller 131 | - Vendor Entity 132 | - Vendor Model 133 | - Vendor View 134 | Vendor Config: 135 | - Config 136 | - Service 137 | - Vendor Config 138 | - Vendor Controller 139 | - Vendor Entity 140 | - Vendor Model 141 | - Vendor View 142 | Vendor Entity: 143 | - Service 144 | - Vendor Config 145 | - Vendor Controller 146 | - Vendor Entity 147 | - Vendor Model 148 | - Vendor View 149 | Vendor View: 150 | - Service 151 | - Vendor Config 152 | - Vendor Controller 153 | - Vendor Entity 154 | - Vendor Model 155 | - Vendor View 156 | skip_violations: 157 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | sets([SetList::DEAD_CODE, LevelSetList::UP_TO_PHP_74, PHPUnitSetList::PHPUNIT_SPECIFIC_METHOD, PHPUnitSetList::PHPUNIT_80]); 41 | $rectorConfig->parallel(); 42 | // The paths to refactor (can also be supplied with CLI arguments) 43 | $rectorConfig->paths([ 44 | __DIR__ . '/src/', 45 | __DIR__ . '/tests/', 46 | ]); 47 | 48 | // Include Composer's autoload - required for global execution, remove if running locally 49 | $rectorConfig->autoloadPaths([ 50 | __DIR__ . '/vendor/autoload.php', 51 | ]); 52 | 53 | // Do you need to include constants, class aliases, or a custom autoloader? 54 | $rectorConfig->bootstrapFiles([ 55 | realpath(getcwd()) . '/vendor/codeigniter4/framework/system/Test/bootstrap.php', 56 | ]); 57 | 58 | if (is_file(__DIR__ . '/phpstan.neon.dist')) { 59 | $rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon.dist'); 60 | } 61 | 62 | // Set the target version for refactoring 63 | $rectorConfig->phpVersion(PhpVersion::PHP_74); 64 | 65 | // Auto-import fully qualified class names 66 | $rectorConfig->importNames(); 67 | 68 | // Are there files or rules you need to skip? 69 | $rectorConfig->skip([ 70 | __DIR__ . '/src/Views', 71 | 72 | JsonThrowOnErrorRector::class, 73 | StringifyStrNeedlesRector::class, 74 | 75 | // Note: requires php 8 76 | RemoveUnusedPromotedPropertyRector::class, 77 | 78 | // Ignore tests that might make calls without a result 79 | RemoveEmptyMethodCallRector::class => [ 80 | __DIR__ . '/tests', 81 | ], 82 | 83 | // Ignore files that should not be namespaced 84 | NormalizeNamespaceByPSR4ComposerAutoloadRector::class => [ 85 | __DIR__ . '/src/Helpers', 86 | ], 87 | 88 | // May load view files directly when detecting classes 89 | StringClassNameToClassConstantRector::class, 90 | 91 | // May be uninitialized on purpose 92 | AddDefaultValueForUndefinedVariableRector::class, 93 | 94 | // Rector keeps trying to remove a necessary "if" condition, 2022-08-09 95 | RemoveAlwaysTrueIfConditionRector::class => [ 96 | __DIR__ . '/src/Filters/ThemesFilter.php', 97 | ], 98 | ]); 99 | $rectorConfig->rule(SimplifyUselessVariableRector::class); 100 | $rectorConfig->rule(RemoveAlwaysElseRector::class); 101 | $rectorConfig->rule(CountArrayToEmptyArrayComparisonRector::class); 102 | $rectorConfig->rule(ForToForeachRector::class); 103 | $rectorConfig->rule(ChangeNestedForeachIfsToEarlyContinueRector::class); 104 | $rectorConfig->rule(ChangeIfElseValueAssignToEarlyReturnRector::class); 105 | $rectorConfig->rule(SimplifyStrposLowerRector::class); 106 | $rectorConfig->rule(CombineIfRector::class); 107 | $rectorConfig->rule(SimplifyIfReturnBoolRector::class); 108 | $rectorConfig->rule(InlineIfToExplicitIfRector::class); 109 | $rectorConfig->rule(PreparedValueToEarlyReturnRector::class); 110 | $rectorConfig->rule(ShortenElseIfRector::class); 111 | $rectorConfig->rule(SimplifyIfElseToTernaryRector::class); 112 | $rectorConfig->rule(UnusedForeachValueToArrayKeysRector::class); 113 | $rectorConfig->rule(ChangeArrayPushToArrayAssignRector::class); 114 | $rectorConfig->rule(UnnecessaryTernaryExpressionRector::class); 115 | $rectorConfig->rule(AddPregQuoteDelimiterRector::class); 116 | $rectorConfig->rule(SimplifyRegexPatternRector::class); 117 | $rectorConfig->rule(FuncGetArgsToVariadicParamRector::class); 118 | $rectorConfig->rule(MakeInheritedMethodVisibilitySameAsParentRector::class); 119 | $rectorConfig->rule(SimplifyEmptyArrayCheckRector::class); 120 | $rectorConfig->rule(NormalizeNamespaceByPSR4ComposerAutoloadRector::class); 121 | $rectorConfig 122 | ->ruleWithConfiguration(TypedPropertyRector::class, [ 123 | // Set to false if you use in libraries, or it does create breaking changes. 124 | TypedPropertyRector::INLINE_PUBLIC => true, 125 | ]); 126 | }; 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tatter\Themes 2 | Lightweight theme manager for CodeIgniter 4 3 | 4 | [![](https://github.com/tattersoftware/codeigniter4-themes/workflows/PHPUnit/badge.svg)](https://github.com/tattersoftware/codeigniter4-themes/actions/workflows/test.yml) 5 | [![](https://github.com/tattersoftware/codeigniter4-themes/workflows/PHPStan/badge.svg)](https://github.com/tattersoftware/codeigniter4-themes/actions/workflows/analyze.yml) 6 | [![](https://github.com/tattersoftware/codeigniter4-themes/workflows/Deptrac/badge.svg)](https://github.com/tattersoftware/codeigniter4-themes/actions/workflows/inspect.yml) 7 | [![Coverage Status](https://coveralls.io/repos/github/tattersoftware/codeigniter4-themes/badge.svg?branch=develop)](https://coveralls.io/github/tattersoftware/codeigniter4-themes?branch=develop) 8 | 9 | ## Quick Start 10 | 11 | 1. Install with Composer: `> composer require tatter/themes` 12 | 2. Update the database: `> php spark migrate -all` 13 | 3. Seed the database: `> php spark db:seed "Tatter\Themes\Database\Seeds\ThemeSeeder"` 14 | 5. Place theme files in **public/assets/themes/default** 15 | 5. Add theme files to your page, e.g.: `echo view('\Tatter\Themes\Views\css)` 16 | 17 | ## Features 18 | 19 | Provides convenient theme file organization and display for CodeIgniter 4 20 | 21 | ## Installation 22 | 23 | Install easily via Composer to take advantage of CodeIgniter 4's autoloading capabilities 24 | and always be up-to-date: 25 | * `> composer require tatter/themes` 26 | 27 | Or, install manually by downloading the source files and adding the directory to 28 | `app/Config/Autoload.php`. 29 | 30 | Once the files are downloaded and included in the autoload, run any library migrations 31 | to ensure the database is setup correctly: 32 | * `> php spark migrate -all` 33 | 34 | You will also need to seed the database with a default theme: 35 | * `> php spark db:seed "Tatter\Themes\Database\Seeds\ThemeSeeder"` 36 | 37 | ## Dependencies 38 | 39 | `Themes` relies heavily on these libraries; be sure you are familiar with their own 40 | requirements and installation process. 41 | * [Tatter\Assets](https://github.com/tattersoftware/codeigniter4-assets) handles asset discovery and HTML tag injection 42 | * [Tatter\Preferences](https://github.com/tattersoftware/codeigniter4-preferences) allows for user- or session-specific theme changing 43 | * [CodeIgniter\Settings](https://github.com/codeigniter4/settings) (a dependency of `Preferences`) loads and stores theme preferences into persistent storage 44 | 45 | If you plan on allowing users to change their own themes then you will also need to include 46 | an authentication library the provides `codeigniter4/authentication-implementation` (no other 47 | configuration necessary). 48 | 49 | > Read more about CodeIgniter Authentication in the [User Guide](https://codeigniter.com/user_guide/extending/authentication.html). 50 | 51 | ## Usage 52 | 53 | *This library assumes you already have the asset files (CSS and JavaScript) used by your themes.* 54 | 55 | Themes are managed via the database and configured for your application using Filters. 56 | 57 | ### Managing Themes 58 | 59 | Theme files all go into a directory relative to the config property `$directory` from 60 | `Tatter\Assets` (default is **public/assets/**), as defined by a theme's `path`. E.g. 61 | 62 | * public/assets/themes/default/styles.css 63 | * public/assets/themes/default/script.js 64 | * public/assets/themes/dark/header.css 65 | * public/assets/themes/dark/fonts.css 66 | * public/assets/themes/perky/Perky.CSS 67 | 68 | Each theme is an entry in the database `themes` table with the following properties: 69 | 70 | * `name`: A short, unique name used for theme lookup and display, e.g. "Aquatic Journey" 71 | * `path`: The path (relative to the `Assets` directory) to the publicly-available theme files, e.g. "themes/aquatic/" 72 | * `description` (optional): A brief description of this theme's features, mostly useful for allowing user selection, e.g. "A blue theme with deep hues and liquid borders" 73 | * `dark`: Whether this theme is dark (light text, dark backgrounds) or not, e.g. `true` 74 | 75 | You may use the supplied model (`Tatter\Themes\Models\ThemeModel`) to create new themes or 76 | return entities (`Tatter\Themes\Entities\Theme`). The library comes with the `ThemeSeeder` 77 | which will create an initial "Default" theme for you at **public/assets/themes/default/**. 78 | There is also a `themes:add` Spark command to guide you through loading themes from CLI. 79 | 80 | ### Selecting a Theme 81 | 82 | The current theme is determined as follows: 83 | 84 | 1. Is there an authenticated user? Check `Preferences` for that user's theme preference 85 | 2. No authenticated user? Check the Session for a theme preference 86 | 3. Neither of above? Check `Settings` for a stored persistent theme 87 | 4. None of the above? Fall back on the config file: `config('Preferences')->theme` (default value "Default") 88 | 89 | Likewise, you can set the current theme using the corresponding methods (in priority order): 90 | 91 | 1. `preference('theme', $themeName)` (with authenticated user) 92 | 2. `preference('theme', $themeName)` (without authenticated user) 93 | 3. `setting('Preferences.theme, $themeName)` 94 | 4. Create or edit **app/Config/Preferences.php** and add: `public $theme = 'theme_name';` 95 | 96 | To assist with methods 1 & 2 this library comes with a tiny module to display a form and 97 | process user input. You can add the form to an existing page with the Form View: 98 | ```html 99 |
100 | 101 |
102 | ``` 103 | 104 | ...or add the preconfigured ` 110 | 111 | Page theme: 112 | 113 | 114 | 115 | 116 | ``` 117 | 118 | You can pass these additional parameters to the view: 119 | * `$class`: A CSS class to apply to the `