├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── README.md ├── composer.json ├── phpunit.xml ├── public └── .gitkeep ├── src ├── Stylist │ ├── Console │ │ └── PublishAssetsCommand.php │ ├── Facades │ │ ├── StylistFacade.php │ │ └── ThemeFacade.php │ ├── Html │ │ └── ThemeHtmlBuilder.php │ ├── StylistServiceProvider.php │ └── Theme │ │ ├── Exceptions │ │ ├── ThemeJsonNotFoundException.php │ │ └── ThemeNotFoundException.php │ │ ├── Json.php │ │ ├── Loader.php │ │ ├── Stylist.php │ │ ├── Theme.php │ │ └── UrlGenerator.php └── config │ └── config.php └── tests ├── Console └── PublishAssetsCommandTest.php ├── Html └── ThemeHtmlBuilderTest.php ├── Stubs └── Themes │ ├── Child │ ├── theme.json │ └── views │ │ └── partials │ │ └── menu.blade.php │ ├── Overload │ ├── theme.json │ └── views │ │ └── partials │ │ └── menu.blade.php │ └── Parent │ ├── theme.json │ └── views │ └── layouts │ └── application.blade.php ├── TestCase.php └── Theme ├── JsonTest.php ├── LoaderTest.php ├── StylistTest.php └── ThemeTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store 5 | .idea/* 6 | docs/ 7 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | environment: 3 | php: '7' 4 | 5 | filter: 6 | paths: 7 | - 'src/Stylist/*' 8 | excluded_paths: 9 | - 'src/Stylist/StylistServiceProvider.php' 10 | 11 | checks: 12 | php: 13 | code_rating: true 14 | duplication: true 15 | variable_existence: true 16 | useless_calls: true 17 | use_statement_alias_conflict: true 18 | unused_variables: true 19 | unused_properties: true 20 | unused_parameters: true 21 | unused_methods: true 22 | unreachable_code: true 23 | sql_injection_vulnerabilities: true 24 | security_vulnerabilities: true 25 | precedence_mistakes: true 26 | precedence_in_conditions: true 27 | parameter_non_unique: true 28 | no_property_on_interface: true 29 | no_non_implemented_abstract_methods: true 30 | deprecated_code_usage: true 31 | closure_use_not_conflicting: true 32 | closure_use_modifiable: true 33 | avoid_useless_overridden_methods: true 34 | avoid_conflicting_incrementers: true 35 | assignment_of_null_return: true 36 | classes_in_camel_caps: true 37 | avoid_superglobals: true 38 | avoid_usage_of_logical_operators: true 39 | blank_line_after_namespace_declaration: true 40 | encourage_shallow_comparison: false 41 | ensure_lower_case_builtin_functions: true 42 | fix_doc_comments: true 43 | fix_identation_4spaces: true 44 | fix_linefeed: true 45 | fix_php_opening_tag: true 46 | fix_use_statements: 47 | remove_unused: true 48 | preserve_multiple: false 49 | preserve_blanklines: false 50 | order_alphabetically: false 51 | function_in_camel_caps: true 52 | instanceof_class_exists: true 53 | lowercase_basic_constants: true 54 | lowercase_php_keywords: true 55 | no_elseif_statements: true 56 | no_eval: true 57 | no_exit: true 58 | no_global_keyword: true 59 | no_goto: true 60 | no_short_method_names: 61 | minimum: '3' 62 | one_class_per_file: true 63 | psr2_switch_declaration: true 64 | psr2_class_declaration: true 65 | psr2_control_structure_declaration: true 66 | no_error_suppression: true 67 | no_debug_code: true 68 | naming_conventions: 69 | local_variable: '^[a-z][a-zA-Z0-9]*$' 70 | abstract_class_name: ^Abstract|Factory$ 71 | utility_class_name: 'Utils?$' 72 | constant_name: '^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$' 73 | property_name: '^[a-z][a-zA-Z0-9]*$' 74 | method_name: '^(?:[a-z]|__)[a-zA-Z0-9]*$' 75 | parameter_name: '^[a-z][a-zA-Z0-9]*$' 76 | interface_name: '^[A-Z][a-zA-Z0-9]*Interface$' 77 | type_name: '^[A-Z][a-zA-Z0-9]*$' 78 | exception_name: '^[A-Z][a-zA-Z0-9]*Exception$' 79 | isser_method_name: '^(?:is|has|should|may|supports)' 80 | avoid_todo_comments: true 81 | avoid_fixme_comments: true 82 | avoid_duplicate_types: true 83 | too_many_arguments: true 84 | single_namespace_per_use: true 85 | require_scope_for_properties: true 86 | require_scope_for_methods: true 87 | require_php_tag_first: true 88 | require_braces_around_control_structures: true 89 | properties_in_camelcaps: true 90 | prefer_unix_line_ending: true 91 | no_unnecessary_if: true 92 | no_unnecessary_function_call_in_for_loop: true 93 | 94 | tools: 95 | external_code_coverage: true 96 | php_sim: true 97 | php_pdepend: true 98 | php_analyzer: true 99 | php_code_sniffer: 100 | config: 101 | standard: 'PSR2' 102 | sensiolabs_security_checker: true 103 | php_cpd: true 104 | php_changetracking: true 105 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7 5 | 6 | before_script: 7 | - travis_retry composer self-update 8 | - travis_retry composer update --no-interaction --prefer-source 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stylist 2 | ## About 3 | 4 | [![Build Status](https://img.shields.io/travis/floatingpointsoftware/stylist.svg?branch=master)](https://travis-ci.org/floatingpointsoftware/stylist) 5 | 6 | Stylist is a Laravel 5.5+ compatible package for theming your Laravel applications. 7 | 8 | ## Installation 9 | 10 | Via the usual composer command: 11 | 12 | composer require floatingpoint/stylist 13 | 14 | Then, make sure the Stylist service provider is made available to your application by updating your config/app.php: 15 | 16 | 'FloatingPoint\Stylist\StylistServiceProvider', 17 | 18 | You're now ready to go! 19 | 20 | ## Setting up a theme 21 | 22 | In order for Stylist to start using themes, you must register at least one theme with the package, and activate it. 23 | 24 | Stylist::registerPath('/absolute/path/to/theme', true); 25 | 26 | Your theme should contain a theme.json file, which contains some basic information: 27 | 28 | { 29 | "name": "My theme", 30 | "description": "This is my theme. There are many like it, but this one is mine." 31 | } 32 | 33 | Only one theme can be activated at a time, so multiple calls to activate themes, will simply deactivate the previously activated theme. 34 | 35 | So, what happens when you now load views? 36 | 37 | ## How Stylist works 38 | 39 | Everytime you register a new theme and activate it, Stylist then becomes aware of a new location to search for views, stylesheets, 40 | javascripts and image files. Stylist has a few opinions of how to structure your theme directories as well. The reason for this is 41 | so that every theme follows the same approach. For example, when you point Stylist to your theme directory, it should have the 42 | following directories (if it needs them): 43 | 44 | /public/stylesheets 45 | /public/javascripts 46 | /public/images 47 | /views/ 48 | 49 | Then, when you make calls like the following: 50 | 51 | return View::make('layout.application'); 52 | 53 | It'll look in your theme's directory: /views/layout/ for application.blade.php. Simple huh? 54 | 55 | When dealing with assets, Stylist requires the Illuminate\Html library, but instead of using the HTML class, you use Stylist's Theme facade: 56 | 57 | {{ Theme::image('path/to/image.png') }} 58 | 59 | This will look for the image in your theme's directory first and foremost: /public/themes/active-theme/images/path/to/image.png 60 | 61 | This same approach is applied to your styles, js and any other static assets. Whenever you wish to use theme assets, make sure you use the Theme class. 62 | 63 | This means that when you make a call to say, Theme::image, the output url in your HTML will actually look like the following: 64 | 65 | /themes/active-theme/images/path/to/image.png 66 | 67 | Of course, if you don't want Stylist to manage that for you, simply use the usual HTML facade. 68 | 69 | There's one step we're still missing - and that's the publishing of your theme assets. This isn't a necessary step - you can easily 70 | just copy your theme's assets from it's directory, into the appropriate directory in the public directory in Laravel 5. You simply 71 | need to ensure that before you publish, your themes are available and registered. Service providers are a great place to do this. 72 | 73 | public function register() 74 | { 75 | Stylist::registerPaths(Stylist::discover('/path/to/my/themes')); 76 | } 77 | 78 | Then simply run the publish command: 79 | 80 | php artisan stylist:publish 81 | 82 | Or, if you want to publish a select theme: 83 | 84 | php artisan stylist:publish ThemeName 85 | 86 | You'll then have your theme's assets published to their associated directories. It's important to note that the returned array must 87 | contain array elements that point to the the THEME directory, not the theme's ASSETS directories. This is because stylist will try 88 | to work with the theme and its json file, and publish the required files. 89 | 90 | ## Theme inheritance 91 | 92 | Themes can have parent themes. What does this mean? It means that you can request a view, and Stylist will first look to the child 93 | theme that you have activated, and work its way up a tree. This is really great if you like a particular theme but want to customise 94 | just a single view file. 95 | 96 | ### Defining a parent 97 | 98 | It's very easy to define a parent for a theme. You simply define the parent theme inside your theme.json: 99 | 100 | "parent": "Another theme" 101 | 102 | This will ensure that Stylist will first look in your theme's directories for files and assets, and then look in the parent's theme 103 | directories. If your theme's parent also has a parent, then it will continue looking up the tree until it finds the file. 104 | 105 | Parents do not need to be activated for your theme to make use of them. Only your theme needs to be activated. However, they do need 106 | to be registered. This may be handled by the package that manages your theme, or you can register it yourself. 107 | 108 | ### Stylesheets 109 | 110 | Themes can also inherit stylesheets. In order to do this, a child theme must have a stylesheet name that is identical to its parent. If 111 | this is the case, then the parent CSS file will be loaded first, followed by the child's css theme. This makes it very easy to create 112 | "skins" for themes, by simply overloading certain styles. 113 | 114 | ## Helper methods 115 | 116 | Stylist has a few helper methods as well, to ease development. 117 | 118 | Theme::url() 119 | 120 | When used in a view, this method would return the relative path to a theme's public directory. You can also use it to access any file: 121 | 122 | Theme::url('favicon.ico') 123 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "floatingpoint/stylist", 3 | "description": "Laravel 5 theming package.", 4 | "authors": [ 5 | { 6 | "name": "Kirk Bushell", 7 | "email": "torm3nt@gmail.com" 8 | }, 9 | { 10 | "name": "Mike Dugan", 11 | "email": "mike@mjdugan.com" 12 | } 13 | ], 14 | "require": { 15 | "php": "^7.0", 16 | "laravelcollective/html": "~5.5", 17 | "illuminate/support": "~5.5" 18 | }, 19 | "require-dev": { 20 | "mockery/mockery": "^0.9.5", 21 | "orchestra/testbench": "~3.0", 22 | "phpunit/phpunit": "~6.0" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "FloatingPoint\\Stylist\\": "src/Stylist", 27 | "Tests\\": "tests" 28 | } 29 | }, 30 | "minimum-stability": "dev", 31 | "config": { 32 | "preferred-install": "dist" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floatingpointsoftware/stylist/09ebe907cc8cacc22572f4e9d95cbfa7d1f1df09/public/.gitkeep -------------------------------------------------------------------------------- /src/Stylist/Console/PublishAssetsCommand.php: -------------------------------------------------------------------------------- 1 | setupThemes(); 33 | $this->publishAssets(); 34 | 35 | $this->info('Assets published.'); 36 | } 37 | 38 | /** 39 | * Fires the publishing event, then works through the array of returned paths and registers 40 | * themes for those which are valid (aka, contain a theme.json file). 41 | */ 42 | protected function setupThemes() 43 | { 44 | $this->laravel['events']->dispatch('stylist.publishing'); 45 | 46 | $themes = Stylist::themes(); 47 | 48 | foreach ($themes as $theme) { 49 | $path = $theme->getPath(); 50 | 51 | if ($this->laravel['files']->exists($path.'assets/')) { 52 | $this->laravel['stylist']->registerPath($path); 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * Copies the assets for those themes which were successfully registered with stylist. 59 | */ 60 | protected function publishAssets() 61 | { 62 | $themes = $this->laravel['stylist']->themes(); 63 | $requestedTheme = $this->argument('theme'); 64 | 65 | if ($requestedTheme) { 66 | $theme = $this->laravel['stylist']->get($requestedTheme); 67 | 68 | return $this->publishSingle($theme); 69 | } 70 | 71 | foreach ($themes as $theme) { 72 | $this->publishSingle($theme); 73 | } 74 | } 75 | 76 | /** 77 | * Publish a single theme's assets. 78 | * 79 | * @param Theme $theme 80 | */ 81 | protected function publishSingle(Theme $theme) 82 | { 83 | $themePath = public_path('themes/' . $theme->getAssetPath()); 84 | 85 | $this->laravel['files']->copyDirectory($theme->getPath().'/assets/', $themePath); 86 | 87 | $this->info($theme->getName().' assets published.'); 88 | } 89 | 90 | /** 91 | * Developers can publish a specific theme should they wish. 92 | * 93 | * @return array 94 | */ 95 | public function getArguments() 96 | { 97 | return [ 98 | ['theme', InputArgument::OPTIONAL, 'Name of the theme you wish to publish'] 99 | ]; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Stylist/Facades/StylistFacade.php: -------------------------------------------------------------------------------- 1 | html = $html; 27 | $this->url = $url; 28 | } 29 | 30 | /** 31 | * Generate a link to a JavaScript file. 32 | * 33 | * @param string $url 34 | * @param array $attributes 35 | * @param bool $secure 36 | * @return string 37 | */ 38 | public function script($url, $attributes = array(), $secure = null) 39 | { 40 | return $this->html->script($this->assetUrl($url), $attributes, $secure); 41 | } 42 | 43 | /** 44 | * Generate a link to a CSS file. With Stylist, this could actually generate 45 | * numerous style tags, due to CSS inheritance requirements. 46 | * 47 | * @param string $url 48 | * @param array $attributes 49 | * @param bool $secure 50 | * @return string 51 | */ 52 | public function style($url, $attributes = array(), $secure = null) 53 | { 54 | $styles = []; 55 | $theme = StylistFacade::current(); 56 | 57 | // If our theme has a parent, we want its stylesheet, as well. 58 | // @todo: This is dog-ugly - need to figure out a better approach. 59 | if ($theme->hasParent()) { 60 | $parent = StylistFacade::get($theme->getParent()); 61 | StylistFacade::activate($parent); 62 | $styles[] = $this->style($url, $attributes, $secure); 63 | StylistFacade::activate($theme); 64 | } 65 | 66 | $styles[] = $this->html->style($this->assetUrl($url), $attributes, $secure); 67 | 68 | return implode("\n", $styles); 69 | } 70 | 71 | /** 72 | * Generate an HTML image element. 73 | * 74 | * @param string $url 75 | * @param string $alt 76 | * @param array $attributes 77 | * @param bool $secure 78 | * @return string 79 | */ 80 | public function image($url, $alt = null, $attributes = array(), $secure = null) 81 | { 82 | return $this->html->image($this->assetUrl($url), $alt, $attributes, $secure); 83 | } 84 | 85 | /** 86 | * Returns the theme's public URI location. This is not a full URL. If you wish 87 | * for a full URL, simply add the site's URL configuration to this path. 88 | * 89 | * @param string $file 90 | * @return string 91 | */ 92 | public function url($file = '') 93 | { 94 | return url($this->assetUrl($file)); 95 | } 96 | 97 | /** 98 | * Generate a HTML link to an asset. 99 | * 100 | * @param string $url 101 | * @param string $title 102 | * @param array $attributes 103 | * @param bool $secure 104 | * @return string 105 | */ 106 | public function linkAsset($url, $title = null, $attributes = array(), $secure = null) 107 | { 108 | return $this->html->linkAsset($this->assetUrl($url), $title, $attributes, $secure); 109 | } 110 | 111 | /** 112 | * Do a few checks to get the theme path for a given asset url. 113 | * 114 | * @param string $url 115 | * @return string 116 | */ 117 | protected function assetUrl($url) 118 | { 119 | if ($this->url->isValidUrl($url)) { 120 | return $url; 121 | } 122 | 123 | $theme = StylistFacade::current(); 124 | 125 | if ($theme) { 126 | $themePath = $theme->getAssetPath(); 127 | 128 | $url = "themes/$themePath/$url"; 129 | } 130 | 131 | return $url; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Stylist/StylistServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerConfiguration(); 31 | $this->registerStylist(); 32 | $this->registerAliases(); 33 | $this->registerThemeBuilder(); 34 | $this->registerCommands(); 35 | } 36 | 37 | /** 38 | * Boot the package, in this case also discovering any themes required by stylist. 39 | */ 40 | public function boot() 41 | { 42 | $this->bootThemes(); 43 | } 44 | 45 | /** 46 | * Once the provided has booted, we can now look at configuration and see if there's 47 | * any paths defined to automatically load and register the required themes. 48 | */ 49 | protected function bootThemes() 50 | { 51 | $stylist = $this->app['stylist']; 52 | $paths = $this->app['config']->get('stylist.themes.paths', []); 53 | 54 | foreach ($paths as $path) { 55 | $themePaths = $stylist->discover($path); 56 | $stylist->registerPaths($themePaths); 57 | } 58 | 59 | $theme = $this->app['config']->get('stylist.themes.activate', null); 60 | 61 | if (!is_null($theme)) { 62 | $stylist->activate($theme, true); 63 | } 64 | } 65 | 66 | /** 67 | * Sets up the object that will be used for theme registration calls. 68 | */ 69 | protected function registerStylist() 70 | { 71 | $this->app->singleton('stylist', function($app) 72 | { 73 | return new Stylist(new Loader, $app); 74 | }); 75 | } 76 | 77 | /** 78 | * Create the binding necessary for the theme html builder. 79 | */ 80 | protected function registerThemeBuilder() 81 | { 82 | $this->app->singleton('stylist.theme', function($app) 83 | { 84 | return new ThemeHtmlBuilder($app['html'], $app['url']); 85 | }); 86 | } 87 | 88 | /** 89 | * Stylist class should be accessible from global scope for ease of use. 90 | */ 91 | private function registerAliases() 92 | { 93 | $aliasLoader = AliasLoader::getInstance(); 94 | 95 | $aliasLoader->alias('Stylist', 'FloatingPoint\Stylist\Facades\StylistFacade'); 96 | $aliasLoader->alias('Theme', 'FloatingPoint\Stylist\Facades\ThemeFacade'); 97 | 98 | $this->app->alias('stylist', 'FloatingPoint\Stylist\Theme\Stylist'); 99 | } 100 | 101 | /** 102 | * Register the commands available to the package. 103 | */ 104 | private function registerCommands() 105 | { 106 | $this->commands( 107 | 'FloatingPoint\Stylist\Console\PublishAssetsCommand' 108 | ); 109 | } 110 | 111 | /** 112 | * Setup the configuration that can be used by stylist. 113 | */ 114 | protected function registerConfiguration() 115 | { 116 | $this->publishes([ 117 | __DIR__ . '/../config/config.php' => config_path('stylist.php') 118 | ]); 119 | } 120 | 121 | /** 122 | * An array of classes that Stylist provides. 123 | * 124 | * @return array 125 | */ 126 | public function provides() 127 | { 128 | return array_merge(parent::provides(), [ 129 | 'Stylist', 130 | 'Theme' 131 | ]); 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/Stylist/Theme/Exceptions/ThemeJsonNotFoundException.php: -------------------------------------------------------------------------------- 1 | message = "theme.json file does not exist at [$path]."; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Stylist/Theme/Exceptions/ThemeNotFoundException.php: -------------------------------------------------------------------------------- 1 | message = "Theme [$themeName] is not registered with Stylist."; 10 | } 11 | } -------------------------------------------------------------------------------- /src/Stylist/Theme/Json.php: -------------------------------------------------------------------------------- 1 | themePath = $themePath; 28 | } 29 | 30 | /** 31 | * Retrieves the JSON-decoded json file. 32 | * 33 | * @return array 34 | */ 35 | public function getJson() 36 | { 37 | if ($this->json) { 38 | return $this->json; 39 | } 40 | 41 | $themeJsonPath = $this->themePath.'/theme.json'; 42 | 43 | if (!File::exists($themeJsonPath)) { 44 | throw new ThemeJsonNotFoundException($this->themePath); 45 | } 46 | 47 | return $this->json = json_decode(File::get($themeJsonPath)); 48 | } 49 | 50 | /** 51 | * Returns the value for a specific json attribute. 52 | * 53 | * @param string $attribute 54 | * @return mixed 55 | */ 56 | public function getJsonAttribute($attribute) 57 | { 58 | $json = $this->getJson(); 59 | 60 | if (isset($json->$attribute)) { 61 | return $json->$attribute; 62 | } 63 | 64 | return null; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Stylist/Theme/Loader.php: -------------------------------------------------------------------------------- 1 | getJsonAttribute('name'), 29 | $themeJson->getJsonAttribute('description'), 30 | $path, 31 | $themeJson->getJsonAttribute('parent') 32 | ); 33 | } 34 | 35 | /** 36 | * Creates a new theme instance based on the cache object provided. 37 | * 38 | * @param stdClass $cache 39 | * @return Theme 40 | */ 41 | public function fromCache(\stdClass $cache) 42 | { 43 | return new Theme( 44 | $cache->name, 45 | $cache->description, 46 | $cache->path, 47 | $cache->parent 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Stylist/Theme/Stylist.php: -------------------------------------------------------------------------------- 1 | themeLoader = $themeLoader; 65 | $this->app = $app; 66 | $this->view = $this->app->make('view'); 67 | } 68 | 69 | /** 70 | * Register a new theme based on its path. An optional 71 | * parameter allows the theme to be activated as soon as its registered. 72 | * 73 | * @param Theme $theme 74 | * @param bool $activate 75 | */ 76 | public function register(Theme $theme, $activate = false) 77 | { 78 | if (!$this->has($theme->getName())) { 79 | $this->themes[] = $theme; 80 | } 81 | 82 | if ($activate) { 83 | $this->activate($theme); 84 | } 85 | } 86 | 87 | /** 88 | * Register a theme with Stylist based on its path. 89 | * 90 | * @param string $path 91 | * @param boolean $activate 92 | */ 93 | public function registerPath($path, $activate = false) 94 | { 95 | $realPath = realpath($path); 96 | $theme = $this->themeLoader->fromPath($realPath); 97 | 98 | $this->register($theme, $activate); 99 | } 100 | 101 | /** 102 | * Register a number of themes based on the array of paths provided. 103 | * 104 | * @param array $paths 105 | */ 106 | public function registerPaths(array $paths) 107 | { 108 | foreach ($paths as $path) { 109 | $this->registerPath($path); 110 | } 111 | } 112 | 113 | /** 114 | * Activate a theme. Activation can be done by the theme's name, or via a Theme object. 115 | * 116 | * @param string|Theme $theme 117 | * @throws ThemeNotFoundException 118 | */ 119 | public function activate($theme) 120 | { 121 | if (!$theme instanceof $theme) { 122 | $theme = $this->get($theme); 123 | } 124 | 125 | $this->activeTheme = $theme; 126 | 127 | $this->activateFinderPaths($theme); 128 | } 129 | 130 | /** 131 | * Activates the view finder paths for a theme and its parents. 132 | * 133 | * @param Theme $theme 134 | */ 135 | protected function activateFinderPaths(Theme $theme) 136 | { 137 | if ($theme->hasParent()) { 138 | $this->activateFinderPaths($this->get($theme->getParent())); 139 | } 140 | 141 | $this->view->getFinder()->prependLocation($theme->getPath().'/views/'); 142 | } 143 | 144 | /** 145 | * Returns the currently active theme. 146 | * 147 | * @return Theme 148 | */ 149 | public function current() 150 | { 151 | return $this->activeTheme; 152 | } 153 | 154 | /** 155 | * Checks to see whether a theme by a given name has been registered. 156 | * 157 | * @param string $themeName 158 | * @return bool 159 | */ 160 | public function has($themeName) 161 | { 162 | foreach ($this->themes as $theme) { 163 | if ($theme->getName() == $themeName) { 164 | return true; 165 | } 166 | } 167 | 168 | return false; 169 | } 170 | 171 | /** 172 | * Retrieves a theme based on its name. If no theme is found it'll throw a ThemeNotFoundException. 173 | * 174 | * @param string $themeName 175 | * @return Theme 176 | * @throws ThemeNotFoundException 177 | */ 178 | public function get($themeName) 179 | { 180 | foreach ($this->themes as $theme) { 181 | if ($theme->getName() === $themeName) { 182 | return $theme; 183 | } 184 | } 185 | 186 | throw new ThemeNotFoundException($themeName); 187 | } 188 | 189 | /** 190 | * Returns an array of themes that have been registered with Stylist. 191 | * 192 | * @return array 193 | */ 194 | public function themes() 195 | { 196 | return $this->themes; 197 | } 198 | 199 | /** 200 | * Searches for theme.json files within the directory structure specified by $directory and 201 | * returns the theme locations found. This method means that themes do not need to be manually 202 | * registered, however - it is a costly operation, and should be cached once you've found the 203 | * themes. 204 | * 205 | * @param $directory 206 | * @return array Returns an array of theme directory locations 207 | */ 208 | public function discover($directory) 209 | { 210 | $searchString = $directory.'/theme.json'; 211 | 212 | $files = str_replace('theme.json', '', $this->rglob($searchString)); 213 | 214 | return $files; 215 | } 216 | 217 | /** 218 | * Will glob recursively for a files specified within the pattern. 219 | * 220 | * @param string $pattern 221 | * @param int $flags 222 | * @return array 223 | */ 224 | protected function rglob($pattern, $flags = 0) { 225 | $files = glob($pattern, $flags); 226 | 227 | if ($files) { 228 | return $files; 229 | } 230 | 231 | $files = []; 232 | 233 | $possibleFiles = glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT); 234 | 235 | if ($possibleFiles === false) { 236 | $possibleFiles = []; 237 | } 238 | 239 | foreach ($possibleFiles as $dir) { 240 | $files = array_merge($files, $this->rglob($dir.'/'.basename($pattern), $flags)); 241 | } 242 | 243 | return $files; 244 | } 245 | 246 | /** 247 | * Caches the themes provide. This is particularly handy if you use the discover method 248 | * to search your entire installation for themes. Whenever this method is called, it 249 | * will wipe the old cache file and re-write the new cache. 250 | * 251 | * @param array $themes Must consist of Theme objects 252 | */ 253 | public function cache(array $themes = []) 254 | { 255 | $cacheJson = []; 256 | 257 | foreach ($themes as $theme) { 258 | $cacheJson[] = $theme->toArray(); 259 | } 260 | 261 | Cache::forever($this->cacheKey, json_encode($cacheJson)); 262 | } 263 | 264 | /** 265 | * Clear any cache stylist may currently be using for configuration. 266 | * 267 | * @return void 268 | */ 269 | public function clearCache() 270 | { 271 | Cache::forget($this->cacheKey); 272 | } 273 | 274 | /** 275 | * Sets up stylist to use themes from the cache. Stylist uses Laravel's own caching 276 | * mechanisms, so this could be stored on the disk, in memcache or elsewhere. 277 | */ 278 | public function setupFromCache() 279 | { 280 | if (!Cache::has($this->cacheKey)) { 281 | return; 282 | } 283 | 284 | $this->themes = []; 285 | $cachedThemes = json_decode(Cache::get($this->cacheKey)); 286 | 287 | foreach ($cachedThemes as $cachedTheme) { 288 | $this->themes[] = $this->themeLoader->fromCache($cachedTheme); 289 | } 290 | } 291 | 292 | /** 293 | * Return the key used for cache storage. 294 | * 295 | * @return string 296 | */ 297 | public function cacheKey() 298 | { 299 | return $this->cacheKey; 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/Stylist/Theme/Theme.php: -------------------------------------------------------------------------------- 1 | name = $name; 48 | $this->description = $description; 49 | $this->parent = $parent; 50 | $this->path = $path; 51 | } 52 | 53 | /** 54 | * @return string 55 | */ 56 | public function getDescription() 57 | { 58 | return $this->description; 59 | } 60 | 61 | /** 62 | * @return string 63 | */ 64 | public function getName() 65 | { 66 | return $this->name; 67 | } 68 | 69 | /** 70 | * @return null|string 71 | */ 72 | public function getParent() 73 | { 74 | return $this->parent; 75 | } 76 | 77 | /** 78 | * @return string 79 | */ 80 | public function getPath() 81 | { 82 | return $this->path; 83 | } 84 | 85 | /** 86 | * Determines whether or not a theme has a parent. 87 | * 88 | * @return bool 89 | */ 90 | public function hasParent() 91 | { 92 | return !!$this->parent; 93 | } 94 | 95 | /** 96 | * Return the asset path to the theme. 97 | * 98 | * @return string 99 | */ 100 | public function getAssetPath() 101 | { 102 | return Str::slug($this->getName()); 103 | } 104 | 105 | /** 106 | * Returns the theme object as an array, containing all theme information. 107 | * 108 | * @return array 109 | */ 110 | public function toArray() 111 | { 112 | return get_object_vars($this); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Stylist/Theme/UrlGenerator.php: -------------------------------------------------------------------------------- 1 | isValidUrl($path)) return $path; 24 | 25 | // Once we get the root URL, we will check to see if it contains an index.php 26 | // file in the paths. If it does, we will remove it since it is not needed 27 | // for asset paths, but only for routes to endpoints in the application. 28 | $root = $this->getRootUrl($this->getScheme($secure)); 29 | 30 | $theme = Stylist::current(); 31 | 32 | return $this->removeIndex($root).'/themes/'.$theme->getPath().'/'.trim($path, '/'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/config/config.php: -------------------------------------------------------------------------------- 1 | [ 5 | /** 6 | * Absolute paths as to where stylist can discover themes. 7 | */ 8 | 'paths' => [ 9 | 10 | ], 11 | 12 | /** 13 | * Specify the name of the theme that you wish to activate. This should be the same 14 | * as the theme name that is defined within that theme's json file. 15 | */ 16 | 'activate' => null 17 | ] 18 | ]; 19 | -------------------------------------------------------------------------------- /tests/Console/PublishAssetsCommandTest.php: -------------------------------------------------------------------------------- 1 | app->make('Illuminate\Contracts\Console\Kernel'); 15 | 16 | File::shouldReceive('exists')->andReturn(true)->times(12); 17 | File::shouldReceive('get')->times(9); 18 | File::shouldReceive('copyDirectory')->times(4); 19 | 20 | // Action 21 | $artisan->call('stylist:publish'); 22 | 23 | // Assert 24 | // $this->assertTrue($this->app['files']->exists(public_path('themes/child-theme'))); 25 | // $this->assertFalse($this->app['files']->exists(public_path('themes/parent-theme'))); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Html/ThemeHtmlBuilderTest.php: -------------------------------------------------------------------------------- 1 | builder = new ThemeHtmlBuilder($this->app['html'], $this->app['url']);; 15 | 16 | StylistFacade::registerPath(__DIR__.'/../Stubs/Themes/Parent'); 17 | StylistFacade::activate('Parent theme'); 18 | } 19 | 20 | public function testScriptUrlCreation() 21 | { 22 | $script = $this->builder->script('script.js'); 23 | 24 | $this->assertContains('/themes/parent-theme/script.js', (string) $script); 25 | } 26 | 27 | public function testStyleUrlCreation() 28 | { 29 | $style = $this->builder->script('css/app.css'); 30 | 31 | $this->assertContains('/themes/parent-theme/css/app.css', (string) $style); 32 | } 33 | 34 | public function testImageUrlCreation() 35 | { 36 | $image = $this->builder->image('images/my-image.png'); 37 | 38 | $this->assertContains('/themes/parent-theme/images/my-image.png', (string) $image); 39 | } 40 | 41 | public function testHtmlLinkAssetCreation() 42 | { 43 | $flashLink = $this->builder->linkAsset('swf/video.swf'); 44 | 45 | $this->assertContains('/themes/parent-theme/swf/video.swf', (string) $flashLink); 46 | } 47 | 48 | public function testAssetUrlResponse() 49 | { 50 | $this->assertEquals(url('themes/parent-theme/'), $this->builder->url()); 51 | $this->assertEquals(url('themes/parent-theme/favicon.ico'), $this->builder->url('favicon.ico')); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Stubs/Themes/Child/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Child theme", 3 | "description": "This is a child theme.", 4 | "parent": "Parent theme" 5 | } -------------------------------------------------------------------------------- /tests/Stubs/Themes/Child/views/partials/menu.blade.php: -------------------------------------------------------------------------------- 1 | Child -------------------------------------------------------------------------------- /tests/Stubs/Themes/Overload/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Overloader", 3 | "description": "This is the overloaded theme.", 4 | "parent": "Child theme" 5 | } -------------------------------------------------------------------------------- /tests/Stubs/Themes/Overload/views/partials/menu.blade.php: -------------------------------------------------------------------------------- 1 | Overload -------------------------------------------------------------------------------- /tests/Stubs/Themes/Parent/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Parent theme", 3 | "description": "This is a parent theme." 4 | } 5 | -------------------------------------------------------------------------------- /tests/Stubs/Themes/Parent/views/layouts/application.blade.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floatingpointsoftware/stylist/09ebe907cc8cacc22572f4e9d95cbfa7d1f1df09/tests/Stubs/Themes/Parent/views/layouts/application.blade.php -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | init(); 19 | } 20 | 21 | protected function init() 22 | { 23 | // Stub/template method - overloadable by children 24 | } 25 | 26 | protected function getPackageProviders($app) 27 | { 28 | return [ 29 | 'Collective\Html\HtmlServiceProvider', 30 | 'FloatingPoint\Stylist\StylistServiceProvider', 31 | ]; 32 | } 33 | 34 | protected function getPackageAliases($app) 35 | { 36 | return [ 37 | 'Stylist' => 'FloatingPoint\Stylist\Facades\StylistFacade', 38 | 'Theme' => 'FloatingPoint\Stylist\Facades\ThemeFacade', 39 | ]; 40 | } 41 | 42 | protected function getApplicationAliases($app) 43 | { 44 | $aliases = parent::getApplicationAliases($app); 45 | 46 | $aliases['Stylist'] = 'FloatingPoint\Stylist\Facades\StylistFacade'; 47 | 48 | return $aliases; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/Theme/JsonTest.php: -------------------------------------------------------------------------------- 1 | themeJson = new Json(__DIR__.'/../Stubs/Themes/Parent'); 13 | } 14 | 15 | public function testJsonRetrievalForExistingTheme() 16 | { 17 | $json = $this->themeJson->getJson(); 18 | 19 | $this->assertEquals('Parent theme', $json->name); 20 | } 21 | 22 | public function testJsonAttributeRetrieval() 23 | { 24 | $this->themeJson->getJson(); 25 | 26 | $this->assertEquals('Parent theme', $this->themeJson->getJsonAttribute('name')); 27 | $this->assertEquals('This is a parent theme.', $this->themeJson->getJsonAttribute('description')); 28 | } 29 | 30 | /** 31 | * @expectedException FloatingPoint\Stylist\Theme\Exceptions\ThemeJsonNotFoundException 32 | */ 33 | public function testThemeFileMissing() 34 | { 35 | $json = new Json('path/that/doesnt/exist'); 36 | 37 | $json->getJson(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Theme/LoaderTest.php: -------------------------------------------------------------------------------- 1 | loader = new Loader(); 13 | } 14 | 15 | public function testFromPath() 16 | { 17 | $theme = $this->loader->fromPath(__DIR__.'/../Stubs/Themes/Parent/'); 18 | 19 | $this->assertEquals('Parent theme', $theme->getName()); 20 | $this->assertEquals('This is a parent theme.', $theme->getDescription()); 21 | } 22 | 23 | public function testFromCache() 24 | { 25 | $cachedTheme = new \stdClass; 26 | $cachedTheme->name = 'name'; 27 | $cachedTheme->description = 'description'; 28 | $cachedTheme->path = 'path'; 29 | $cachedTheme->parent = 'parent'; 30 | 31 | $theme = $this->loader->fromCache($cachedTheme); 32 | 33 | $this->assertEquals('name', $theme->getName()); 34 | $this->assertEquals('description', $theme->getDescription()); 35 | $this->assertEquals('path', $theme->getPath()); 36 | $this->assertEquals('parent', $theme->getParent()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Theme/StylistTest.php: -------------------------------------------------------------------------------- 1 | app); 13 | $theme = new Theme('n', 'd', 'path'); 14 | 15 | $stylist->register($theme, true); 16 | 17 | $this->assertEquals($stylist->get('n'), $theme); 18 | $this->assertEquals($stylist->current(), $theme); 19 | } 20 | 21 | public function testThemeDiscovery() 22 | { 23 | $stylist = new Stylist(new Loader, $this->app); 24 | $themes = $stylist->discover(__DIR__.'/../Stubs'); 25 | 26 | $this->assertCount(3, $themes); 27 | } 28 | 29 | public function testCacheManagement() 30 | { 31 | $stylist = new Stylist(new Loader, $this->app); 32 | $theme = new Theme('name', 'desc', 'path'); 33 | 34 | $stylist->cache([$theme]); 35 | 36 | // To test cache, we setup a new stylist instance 37 | $stylist = new Stylist(new Loader, $this->app); 38 | $stylist->setupFromCache(); 39 | 40 | $this->assertEquals($theme, $stylist->get('name')); 41 | } 42 | 43 | public function testPathRegistration() 44 | { 45 | $stylist = new Stylist(new Loader, $this->app); 46 | 47 | $stylist->registerPath(__DIR__.'/../Stubs/Themes/Parent'); 48 | 49 | $this->assertEquals('Parent theme', $stylist->get('Parent theme')->getName()); 50 | } 51 | 52 | public function testMultiplePathRegistrations() 53 | { 54 | $stylist = new Stylist(new Loader, $this->app); 55 | $paths = $stylist->discover(__DIR__.'/../Stubs'); 56 | 57 | $stylist->registerPaths($paths); 58 | $stylist->activate($stylist->get('Child theme')); 59 | 60 | $view = $this->app->make('view'); 61 | 62 | $this->assertEquals('Parent theme', $stylist->get('Parent theme')->getName()); 63 | $this->assertEquals('Child theme', $stylist->get('Child theme')->getName()); 64 | $this->assertTrue($view->exists('partials.menu')); // should pull this from the child theme 65 | $this->assertTrue($view->exists('layouts.application')); // should pull this from the parent theme 66 | } 67 | 68 | /** 69 | * @expectedException FloatingPoint\Stylist\Theme\Exceptions\ThemeNotFoundException 70 | */ 71 | public function testInvalidTheme() 72 | { 73 | $stylist = new Stylist(new Loader, $this->app); 74 | $stylist->get('invalidtheme'); 75 | } 76 | 77 | public function testThemeViewIsOverloadable() 78 | { 79 | $stylist = new Stylist(new Loader, $this->app); 80 | $paths = $stylist->discover(__DIR__.'/../Stubs'); 81 | 82 | $stylist->registerPaths($paths); 83 | $stylist->activate($stylist->get('Overloader')); 84 | 85 | $view = $this->app->make('view'); 86 | 87 | $this->assertTrue($view->exists('partials.menu')); 88 | $this->assertSame('Overload', $view->make('partials.menu')->render()); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/Theme/ThemeTest.php: -------------------------------------------------------------------------------- 1 | theme = new Theme('name', 'description', 'path', 'parent'); 11 | } 12 | 13 | public function testGetters() 14 | { 15 | $this->assertEquals('name', $this->theme->getName()); 16 | $this->assertEquals('description', $this->theme->getDescription()); 17 | $this->assertEquals('path', $this->theme->getPath()); 18 | $this->assertEquals('parent', $this->theme->getParent()); 19 | } 20 | 21 | public function testParentCheck() 22 | { 23 | $this->assertTrue($this->theme->hasParent()); 24 | } 25 | } 26 | --------------------------------------------------------------------------------