├── .editorconfig ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── build.xml ├── composer.json ├── composer.lock ├── controller.php ├── phpcs.xml └── src ├── Blade.php ├── Blade ├── Coder.php └── Debugger.php ├── Controller.php ├── Loader.php ├── Module ├── Acf.php └── Tree.php └── Utils.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | dist 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 2.1.2: 2 | * Fixes 3 | 4 | ### 2.1.1: 5 | * Update Brain/Hierarchy to 2.4.0 for CPT templates [#86](https://github.com/soberwp/controller/issues/86) 6 | * Add filter sober/controller/sage/namespace to allow for a custom Sage namespace [#104](https://github.com/soberwp/controller/issues/104) 7 | * Update ACF class to support taxonomy fields by default by using get_queried_object() [#101](https://github.com/soberwp/controller/issues/101) 8 | * Bug fix for ACF class if no fields on page/post [#102](https://github.com/soberwp/controller/issues/102) 9 | * Allow interaction in Controller with the $post object by using $this->post vs $this->data['post'] 10 | 11 | ### 2.1.0: 12 | * Update deps 13 | * Pass in field data from Acf Options under App class 14 | * Change $this->data from private to protected param 15 | * Fix $post bug not appearing in the $this->data 16 | * Fix Controller overriding filter $data 17 | * Add filter to return Acf data as array 18 | * Add __before and __after lifecycles 19 | * @code and @codeif 20 | 21 | ### 2.0.1: 22 | * Fix bug assuming Controllers/ folder name 23 | 24 | ### 2.0.0: 25 | * PSR4 loading 26 | * Template overrides for those underscores 27 | * Pass in field data from Acf automatically 28 | * Debugger to include static methods 29 | * Improve Debugger results 30 | * Dependency injection 31 | * Bug fixes 32 | * Change default path from resources/controllers to app/controllers 33 | 34 | ### 9.0.0-beta.3: 35 | * Changed to Composer package 36 | * Fix for app Controller bug 37 | * Rename base to app 38 | * Change default path from src/controllers to resources/controllers 39 | 40 | ### 9.0.0-beta.2.1: 41 | * Fix for base Controller bug 42 | 43 | ### 9.0.0-beta.2: 44 | * Align with Sage9 versioning 45 | * Enable the use of __construct within the child Class 46 | 47 | ### 1.0.2: 48 | * Prevent public static methods from being passed onto data 49 | * Class alias for use in template 50 | 51 | ### 1.0.1: 52 | * Pass on default post data for posts 53 | * Show $post in the controller debugger 54 | 55 | ### 1.0.0: 56 | * Release 57 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) Darren Jacoby 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Controller 2 | 3 | WordPress package to enable a controller when using Blade with [Sage 9](https://roots.io/sage/) (Please note, Sage 10 uses Composers and not this package.) 4 | 5 | * [Installation](#installation) 6 | * [Setup](#setup) 7 | * [Usage](#usage) 8 | * [Overview](#overview) 9 | * [Basic Controller](#basic-controller) 10 | * [Using Functions](#using-functions) 11 | * [Using Components](#using-components) 12 | * [Inheriting the Tree/Hierarchy](#inheriting-the-treehierarchy) 13 | * [Creating Global Properties](#creating-global-properties) 14 | * [Advanced Custom Fields Module](#advanced-custom-fields-module) 15 | * [Template Override Option](#template-override-option) 16 | * [Lifecycles](#lifecycles) 17 | * [Disable Option](#disable-option) 18 | * [Blade Debugger](#blade-debugger) 19 | * [Blade Coder](#blade-coder) 20 | 21 |
22 | 23 | ## Installation 24 | 25 | ### Composer: 26 | 27 | [Sage](https://roots.io/sage/) ships with Controller. However, should you need to install, browse into the Sage theme directory and run; 28 | 29 | ```shell 30 | $ composer require soberwp/controller:2.1.2 31 | ``` 32 | 33 | ### Upgrading to 2.x.x: 34 | 35 | Please note that versions 2.x.x are newer releases than 9.x.x-beta. The 9 was used to match Sage 9 versioning at the time. 36 | 37 | Controller 2.x.x uses [PSR4 autoloading](https://www.php-fig.org/psr/psr-4/) to load Controller classes. This is considered best practice. You will need to [update the following files](https://github.com/roots/sage/pull/2025/files) from 9.0.0-beta versions. 38 | 39 | Folder `controllers/` changes to `Controllers/`, class file names changes to camelcase `App.php` and `FrontPage.php`. Controller namespaces changes to `namespace App\Controllers;` 40 | 41 | ### Requirements: 42 | 43 | * [PHP](http://php.net/manual/en/install.php) >= 7.0 44 | 45 | ## Setup 46 | 47 | By default Controller uses namespace `Controllers`. 48 | 49 | Controller takes advantage of [PSR-4 autoloading](https://www.php-fig.org/psr/psr-4/). To change the namespace, use the filter below within `functions.php` 50 | 51 | ```php 52 | 53 | add_filter('sober/controller/namespace', function () { 54 | return 'Data'; 55 | }); 56 | ``` 57 | 58 | ## Usage 59 | 60 | ### Overview: 61 | 62 | * Controller class names follow the same hierarchy as WordPress. 63 | * The Controller class name should match the filename 64 | * For example `App.php` should define class as `class App extends Controller` 65 | * Create methods within the Controller Class; 66 | * Use `public function` to return data to the Blade views/s 67 | * The method name becomes the variable name in Blade 68 | * Camel case is converted to snake case. `public function ExampleForUser` in the Controller becomes `$example_for_user` in the Blade template 69 | * If the same method name is declared twice, the latest instance will override the previous 70 | * Use `public static function` to use run the method from your Blade template which returns data. This is useful for loops 71 | * The method name is not converted to snake case 72 | * You access the method using the class name, followed by the method. `public static function Example` in `App.php` can be run in Blade using `App::Example()` 73 | * If the same method name is declared twice, the latest instance will override the previous 74 | * Use `protected function` for internal methods. These will not be exposed to Blade. You can run them within `__construct` 75 | * Dependency injection with type hinting is available through `__construct` 76 | 77 | 78 | The above may sound complicated on first read, so let's take a look at some examples to see how simple Controller is to use. 79 | 80 | ### Basic Controller; 81 | 82 | The following example will expose `$images` to `resources/views/single.blade.php` 83 | 84 | **app/Controllers/Single.php** 85 | 86 | ```php 87 | 112 | @foreach($images as $image) 113 |
  • {{$image['alt']}}
  • 114 | @endforeach 115 | 116 | @endif 117 | ``` 118 | 119 | ### Using Functions; 120 | 121 | You can use static methods to run a function from within your view. 122 | 123 | This is useful if you are within the loop and want to return data for each post item. 124 | 125 | **app/Controllers/Archive.php** 126 | 127 | ```php 128 | post_title; 139 | } 140 | } 141 | ``` 142 | 143 | **resources/views/archive.php** 144 | 145 | ```php 146 | @extends('layouts.app') 147 | 148 | @section('content') 149 | 150 | @while (have_posts()) @php the_post() @endphp 151 | {{ Archive::title() }} 152 | @endwhile 153 | 154 | @endsection 155 | ``` 156 | 157 | ### Using Components; 158 | 159 | You can also create reusable components and include them in any Controller class using PHP traits. 160 | 161 | **app/Controllers/Partials/Images.php** 162 | 163 | ```php 164 | data is set up, but before the class methods are run 320 | } 321 | 322 | public function __after() 323 | { 324 | // runs after all the class methods have run 325 | } 326 | ``` 327 | 328 | ### Disable Option; 329 | 330 | ```php 331 | protected $active = false; 332 | ``` 333 | 334 | ### Blade Debugger; 335 | 336 | In your Blade views, `resources/views`, you can use the following to assist with debugging; 337 | 338 | * `@debug` 339 | * `@dump(__var__)` 340 | 341 | ### Blade Coder; 342 | 343 | In your Blade views, `resources/views`, you can use the following to assist with jump-starting coding; 344 | 345 | * `@code` 346 | * `@code('__name of variable as string__')` 347 | 348 | To wrap the code in if statements, use `@codeif` 349 | 350 | * `@codeif` 351 | * `@codeif('__name of variable as string__')` 352 | 353 | ## Support 354 | 355 | * Follow [@withjacoby](https://twitter.com/withjacoby) on Twitter 356 | * Buy me a beer or pay my rent, [paypal.me/darrenjacoby](https://paypal.me/darrenjacoby) 357 | 358 | ## Updates 359 | 360 | * Change the composer.json version to 2.1.2 361 | * Check [CHANGELOG.md](CHANGELOG.md) for any breaking changes before updating. 362 | 363 | ```shell 364 | $ composer update 365 | ``` 366 | -------------------------------------------------------------------------------- /build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "soberwp/controller", 3 | "type": "package", 4 | "license": "MIT", 5 | "description": "WordPress package to enable a basic controller when using Blade with Sage 9", 6 | "homepage": "https://github.com/soberwp", 7 | "authors": [ 8 | { 9 | "name": "Darren Jacoby", 10 | "email": "darren@jacoby.co.za", 11 | "homepage": "https://github.com/darrenjacoby" 12 | } 13 | ], 14 | "keywords": [ 15 | "wordpress" 16 | ], 17 | "support": { 18 | "issues": "https://github.com/soberwp/controller/issues" 19 | }, 20 | "require": { 21 | "php": ">=5.6.0", 22 | "brain/hierarchy": "^2.4.0" 23 | }, 24 | "require-dev": { 25 | "squizlabs/php_codesniffer": "^3.2" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Sober\\Controller\\": "src/" 30 | }, 31 | "files": [ 32 | "controller.php" 33 | ] 34 | }, 35 | "scripts": { 36 | "test": [ 37 | "phpcs --extensions=php --ignore=vendor/ ." 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "4e348d5744a8bb335cceb88e0ecf3804", 8 | "packages": [ 9 | { 10 | "name": "brain/hierarchy", 11 | "version": "2.4.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/Brain-WP/Hierarchy.git", 15 | "reference": "d0970df66ca36c8a86115401462800d2fe206fd9" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/Brain-WP/Hierarchy/zipball/d0970df66ca36c8a86115401462800d2fe206fd9", 20 | "reference": "d0970df66ca36c8a86115401462800d2fe206fd9", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=5.5" 25 | }, 26 | "require-dev": { 27 | "antecedent/patchwork": "~1.3.0", 28 | "brain/monkey": "~1.2", 29 | "gmazzap/andrew": "~1.0", 30 | "mockery/mockery": "0.9.3", 31 | "phpunit/phpunit": "~4.8", 32 | "symfony/finder": "~2.7.0" 33 | }, 34 | "suggest": { 35 | "symfony/finder": "Allows loading of templates using Symfony finder component." 36 | }, 37 | "type": "library", 38 | "extra": { 39 | "branch-alias": { 40 | "dev-master": "1.2.x-dev", 41 | "dev-dev": "2.0.x-dev" 42 | } 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "Brain\\Hierarchy\\": "src/" 47 | } 48 | }, 49 | "notification-url": "https://packagist.org/downloads/", 50 | "license": [ 51 | "MIT" 52 | ], 53 | "authors": [ 54 | { 55 | "name": "Giuseppe Mazzapica", 56 | "email": "giuseppe.mazzapica@gmail.com" 57 | } 58 | ], 59 | "description": "No-dependencies package that embodies WordPress template hierarchy", 60 | "keywords": [ 61 | "wordpress" 62 | ], 63 | "time": "2018-11-02T07:14:19+00:00" 64 | } 65 | ], 66 | "packages-dev": [ 67 | { 68 | "name": "squizlabs/php_codesniffer", 69 | "version": "3.3.2", 70 | "source": { 71 | "type": "git", 72 | "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", 73 | "reference": "6ad28354c04b364c3c71a34e4a18b629cc3b231e" 74 | }, 75 | "dist": { 76 | "type": "zip", 77 | "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/6ad28354c04b364c3c71a34e4a18b629cc3b231e", 78 | "reference": "6ad28354c04b364c3c71a34e4a18b629cc3b231e", 79 | "shasum": "" 80 | }, 81 | "require": { 82 | "ext-simplexml": "*", 83 | "ext-tokenizer": "*", 84 | "ext-xmlwriter": "*", 85 | "php": ">=5.4.0" 86 | }, 87 | "require-dev": { 88 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" 89 | }, 90 | "bin": [ 91 | "bin/phpcs", 92 | "bin/phpcbf" 93 | ], 94 | "type": "library", 95 | "extra": { 96 | "branch-alias": { 97 | "dev-master": "3.x-dev" 98 | } 99 | }, 100 | "notification-url": "https://packagist.org/downloads/", 101 | "license": [ 102 | "BSD-3-Clause" 103 | ], 104 | "authors": [ 105 | { 106 | "name": "Greg Sherwood", 107 | "role": "lead" 108 | } 109 | ], 110 | "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", 111 | "homepage": "http://www.squizlabs.com/php-codesniffer", 112 | "keywords": [ 113 | "phpcs", 114 | "standards" 115 | ], 116 | "time": "2018-09-23T23:08:17+00:00" 117 | } 118 | ], 119 | "aliases": [], 120 | "minimum-stability": "stable", 121 | "stability-flags": [], 122 | "prefer-stable": false, 123 | "prefer-lowest": false, 124 | "platform": { 125 | "php": ">=5.6.0" 126 | }, 127 | "platform-dev": [] 128 | } 129 | -------------------------------------------------------------------------------- /controller.php: -------------------------------------------------------------------------------- 1 | getClassesToRun() as $class) { 48 | // Create the class on the DI container 49 | $controller = $container->make($class); 50 | 51 | // Set the params required for template param 52 | $controller->__setParams(); 53 | 54 | // Determine template location to expose data 55 | $location = "sage/template/{$controller->__getTemplateParam()}-data/data"; 56 | 57 | // Pass data to filter 58 | add_filter($location, function ($data) use ($container, $class) { 59 | // Recreate the class so that $post is included 60 | $controller = $container->make($class); 61 | 62 | // Params 63 | $controller->__setParams(); 64 | 65 | // Lifecycle 66 | $controller->__before(); 67 | 68 | // Data 69 | $controller->__setData($data); 70 | 71 | // Lifecycle 72 | $controller->__after(); 73 | 74 | // Return 75 | return $controller->__getData(); 76 | }, 10, 2); 77 | } 78 | } 79 | 80 | /** 81 | * Blade 82 | */ 83 | function blade() 84 | { 85 | // Get Sage function 86 | $sage = sage(); 87 | 88 | // Return if function does not exist 89 | if (!$sage) { 90 | return; 91 | } 92 | 93 | // Debugger 94 | $sage('blade')->compiler()->directive('debug', function () { 95 | return ''; 96 | }); 97 | 98 | $sage('blade')->compiler()->directive('dump', function ($param) { 99 | return "dump({$param}); ?>"; 100 | }); 101 | 102 | // Coder 103 | $sage('blade')->compiler()->directive('code', function ($param) { 104 | $param = ($param) ? $param : 'false'; 105 | return ""; 106 | }); 107 | 108 | $sage('blade')->compiler()->directive('codeif', function ($param) { 109 | $param = ($param) ? $param : 'false'; 110 | return ""; 111 | }); 112 | } 113 | 114 | /** 115 | * Hooks 116 | */ 117 | if (function_exists('add_action')) { 118 | add_action('init', __NAMESPACE__ . '\loader'); 119 | add_action('init', __NAMESPACE__ . '\blade'); 120 | } 121 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sober Coding Standards 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Blade.php: -------------------------------------------------------------------------------- 1 | data = $data['__data']['__blade']; 18 | 19 | // Only worry about rewriting data if we have more than one class 20 | if (count($this->data) > 1) { 21 | // Get first item from data array 22 | $first = reset($this->data); 23 | 24 | // Get last item from data array 25 | $last = end($this->data); 26 | 27 | // If last item does not inherit tree and first class is App 28 | if (!$last->tree && $first->class === 'App') { 29 | // Rewrite $this->data with first (App) and last item in array 30 | $this->data = [$first, $last]; 31 | // Else if $last does not inherit tree 32 | } elseif (!$last->tree) { 33 | // Rewrite $this->data with last item in array 34 | $this->data = $last; 35 | } 36 | } 37 | 38 | return $this; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Blade/Coder.php: -------------------------------------------------------------------------------- 1 | codeif = $codeif; 24 | 25 | // Set data from @code('var') 26 | $this->setIncludeData($includes); 27 | 28 | // Set data from $data['__data']['__blade'] 29 | $this->setBladeData($data); 30 | 31 | // Render to view 32 | $this->render(); 33 | } 34 | 35 | private function setIncludeData($includes) 36 | { 37 | $this->includes = $includes; 38 | 39 | if (is_string($this->includes)) { 40 | $this->includes = [$this->includes]; 41 | } 42 | } 43 | 44 | /** 45 | * Increase Indentation 46 | * 47 | * Add two spaces to $this->indentation 48 | */ 49 | private function increaseIndentation() 50 | { 51 | $this->indentation = "{$this->indentation} "; 52 | } 53 | 54 | /** 55 | * Decrease Indentation 56 | * 57 | * Remove two spaces from $this->indentation 58 | */ 59 | private function decreaseIndentation() 60 | { 61 | $this->indentation = substr($this->indentation, 0, -2); 62 | } 63 | 64 | /** 65 | * Render 66 | * 67 | * Loop through $this->data and echo code 68 | */ 69 | private function render() 70 | { 71 | // Map the data results to exclude static methods 72 | $this->data = array_map(function ($item) { 73 | return $item->data; 74 | }, $this->data); 75 | 76 | // Remove the first level of the array so that we are left with flat variables 77 | $this->data = call_user_func_array('array_merge', $this->data); 78 | 79 | // Remove $post by default 80 | unset($this->data['post']); 81 | 82 | // Start @code block 83 | $type = ($this->codeif ? '@codeif' : '@code'); 84 | echo "
    {$type}
    "; 85 | 86 | // Run through each item 87 | foreach ($this->data as $name => $value) { 88 | // Remove the method/returned for data methods 89 | $value = (isset($value->method) ? $value->returned : $value); 90 | 91 | // Router 92 | // @code('var') 93 | if ($this->includes && in_array($name, $this->includes)) { 94 | $this->router($name, $value); 95 | } 96 | 97 | // @code 98 | if (!$this->includes) { 99 | $this->router($name, $value); 100 | } 101 | } 102 | 103 | // End @code block 104 | echo '
    '; 105 | } 106 | 107 | /** 108 | * Router 109 | * 110 | * Route data types to correct methods 111 | */ 112 | private function router($name, $val) 113 | { 114 | // Route object 115 | if (is_object($val)) { 116 | $this->renderObj($name, $val); 117 | } 118 | 119 | // Route indexed array 120 | if (is_array($val) && Utils::isArrayIndexed($val)) { 121 | $this->renderArrIndexed($name, $val); 122 | } 123 | 124 | // Route array with keys 125 | if (is_array($val) && !Utils::isArrayIndexed($val)) { 126 | $this->renderArrKeys($name, $val); 127 | } 128 | 129 | // Route strings/other 130 | if (!is_array($val) && !is_object($val)) { 131 | // Add to $this->code 132 | $this->renderResult($name, $val); 133 | 134 | // Clear out $this-code 135 | $this->code = ''; 136 | 137 | // Exit 138 | return; 139 | } 140 | } 141 | 142 | /** 143 | * Render Object 144 | * 145 | * Render an object 146 | */ 147 | private function renderObj($name, $val) 148 | { 149 | // Get props of object 150 | $props = get_object_vars($val); 151 | 152 | // For each of those props 153 | foreach ($props as $prop_name => $prop_val) { 154 | // Add to $this->code 155 | $this->code = "{$this->code}{$name}->"; 156 | 157 | // Route new values 158 | $this->router($prop_name, $prop_val); 159 | } 160 | } 161 | 162 | /** 163 | * Render Indexed Array 164 | * 165 | * Render an indexed array 166 | */ 167 | private function renderArrIndexed($name, $val) 168 | { 169 | if ($this->codeif) { 170 | // Echo if 171 | echo "{$this->indentation}@if (\${$this->code}$name)
    "; 172 | 173 | // Increase indentation 174 | $this->increaseIndentation(); 175 | } 176 | 177 | // Start foreach 178 | echo "{$this->indentation}@foreach (\${$this->code}$name as \$item)
    "; 179 | 180 | // Clear $this->code 181 | $this->code = ''; 182 | 183 | // Increase indentation 184 | $this->increaseIndentation(); 185 | 186 | // Route next value 187 | foreach ($val as $key_index => $key_val) { 188 | if (count($val) > 1) { 189 | echo "{$this->indentation}[{$key_index}]
    "; 190 | } 191 | $this->router('item', $key_val); 192 | } 193 | 194 | // Decrease indentation 195 | $this->decreaseIndentation(); 196 | 197 | // End foreach 198 | echo "{$this->indentation}@endforeach
    "; 199 | 200 | if ($this->codeif) { 201 | // Decrease indentation 202 | $this->decreaseIndentation(); 203 | 204 | // Echo endif 205 | echo "{$this->indentation}@endif
    "; 206 | } 207 | } 208 | 209 | /** 210 | * Render Array Keys 211 | * 212 | * Render an array with keys 213 | */ 214 | private function renderArrKeys($name, $val) 215 | { 216 | // Foreach value add key 217 | foreach ($val as $key_name => $key_val) { 218 | $this->router("{$this->code}{$name}['{$key_name}']", $key_val); 219 | } 220 | } 221 | 222 | /** 223 | * Render Result 224 | * 225 | * Render the final result 226 | */ 227 | private function renderResult($name, $val) 228 | { 229 | $this->code = "\${$this->code}{$name}"; 230 | 231 | if ($this->codeif) { 232 | // Echo if 233 | echo "{$this->indentation}@if ({$this->code})
    "; 234 | 235 | // Increase indentation 236 | $this->increaseIndentation(); 237 | } 238 | 239 | // Wrap with {{ }} or {!! !!} 240 | if (Utils::doesStringContainMarkup($val)) { 241 | $this->code = "{!! {$this->code} !!}"; 242 | } else { 243 | $this->code = "{{ {$this->code} }}"; 244 | } 245 | 246 | // Echo code 247 | echo "{$this->indentation}{$this->code}
    "; 248 | 249 | if ($this->codeif) { 250 | // Increase indentation 251 | $this->decreaseIndentation(); 252 | 253 | // Echo endif 254 | echo "{$this->indentation}@endif
    "; 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/Blade/Debugger.php: -------------------------------------------------------------------------------- 1 | setBladeData($data); 21 | 22 | // Render to view 23 | $this->render(); 24 | } 25 | 26 | /** 27 | * Render 28 | * 29 | * Loop through $this->data and echo debugger information 30 | */ 31 | private function render() 32 | { 33 | echo ' 34 | '; 43 | 44 | echo '
    @debug
    '; 45 | 46 | foreach ($this->data as $index => $controller) { 47 | // Set the class params for each Controller 48 | $this->controller = $controller; 49 | 50 | // Echo the Controller class name 51 | echo $controller->class; 52 | 53 | // Echo extends tree if the Controller implements tree 54 | if ($controller->tree) { 55 | echo 'extends tree'; 56 | } 57 | 58 | echo ''; 71 | } 72 | 73 | echo '
    '; 74 | } 75 | 76 | /** 77 | * Render Data 78 | * 79 | * Render the current Controller item data 80 | */ 81 | private function renderData() 82 | { 83 | echo '
  • Data
  • '; 126 | } 127 | 128 | /** 129 | * Render Methods 130 | * 131 | * Render the current Controller item methods 132 | */ 133 | private function renderMethods() 134 | { 135 | echo '
  • Methods
  • '; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Controller.php: -------------------------------------------------------------------------------- 1 | class 56 | $this->class = new \ReflectionClass($this); 57 | 58 | // $this->classAcf 59 | if (class_exists('Acf')) { 60 | $this->classAcf = new Acf(); 61 | } 62 | 63 | // $this->template 64 | if (!$this->template) { 65 | $this->template = Utils::convertToKebabCase($this->class->getShortName()); 66 | } 67 | 68 | // $this->tree 69 | if ($this->class->implementsInterface('\Sober\Controller\Module\Tree')) { 70 | $this->tree = true; 71 | } 72 | } 73 | 74 | /** 75 | * Set Controller Data 76 | * 77 | * Set the Controller raw data for this Controller 78 | * @return $this 79 | */ 80 | final public function __setData($incomingData) 81 | { 82 | $this->incomingData = $incomingData; 83 | 84 | // Set the data from the WordPress post if singular to $this->data 85 | $this->__setDataFromPost(); 86 | 87 | // Set the data from Advanced Custom Fields to $this->data 88 | $this->__setDataFromModuleAcf(); 89 | 90 | // Set incoming filter data from Sage to App before Debugger 91 | $this->__setDataFromFilter(); 92 | 93 | // Set the public methods from the class to $this->methods 94 | $this->__setDataFromMethods(); 95 | 96 | // Set debugger data first to use only the raw data from the Controller 97 | $this->__setBladeData(); 98 | 99 | // Set app data to $this->data['__app'] or merge with current data 100 | $this->__setAppData(); 101 | 102 | // Set tree data to $this->data['__tree'] or merge with current data 103 | $this->__setTreeData(); 104 | } 105 | 106 | /** 107 | * Set Data From Post 108 | * 109 | * Set the WordPress post 110 | */ 111 | private function __setDataFromPost() 112 | { 113 | // Only set data from $post to App class 114 | if ($this->template !== 'app') { 115 | return; 116 | } 117 | 118 | // Only continue if $post is available 119 | if (!is_singular()) { 120 | return; 121 | } 122 | 123 | // Set $this->post to allow users to use $this->post->post_title and others 124 | $this->post = get_post(); 125 | 126 | // Set the post array to be included in $this->data 127 | $this->data['post'] = $this->post; 128 | } 129 | 130 | /** 131 | * Set Data From Module Acf 132 | * 133 | * Set the Advanced Custom Fields data automatically 134 | */ 135 | private function __setDatafromModuleAcf() 136 | { 137 | // If $this->acf is not set then return 138 | if (!$this->acf) { 139 | return; 140 | } 141 | 142 | // Set the fields data passed in from Controller 143 | $this->classAcf->setData($this->acf); 144 | 145 | // Get the options page is $this->acf is set to true on App 146 | if ($this->template === 'app' && is_bool($this->acf)) { 147 | $this->classAcf->setDataOptionsPage(); 148 | } 149 | 150 | // Deterime if acf/array filter is enabled and return correct format 151 | $this->classAcf->setDataReturnFormat(); 152 | 153 | // If there is no data return 154 | if (!$this->classAcf->getData()) { 155 | return; 156 | } 157 | 158 | // Merge the data from Acf module 159 | $this->data = array_merge($this->data, $this->classAcf->getData()); 160 | } 161 | 162 | /** 163 | * Set Sage Filter Data 164 | * 165 | * Merge $this->data with $this->incomingData as Sage filters run before -data class filters 166 | */ 167 | private function __setDataFromFilter() 168 | { 169 | if ($this->template === 'app') { 170 | // Merge all incoming data from app to allow Sage add_filter support 171 | $this->data = array_merge($this->data, $this->incomingData); 172 | } 173 | } 174 | 175 | /** 176 | * Set Methods 177 | * 178 | * Set $this->methods with all public methods 179 | */ 180 | private function __setDataFromMethods() 181 | { 182 | // Get all public methods from class 183 | $this->methods = $this->class->getMethods(\ReflectionMethod::IS_PUBLIC); 184 | 185 | // Remove __contruct, __init, __finalize and this class methods from $this->methods 186 | $this->methods = array_filter($this->methods, function ($method) { 187 | return 188 | $method->class !== 'Sober\Controller\Controller' && 189 | $method->name !== '__construct' && 190 | $method->name !== '__before' && 191 | $method->name !== '__after'; 192 | }); 193 | 194 | // Get all public static methods from class 195 | $this->staticMethods = $this->class->getMethods(\ReflectionMethod::IS_STATIC); 196 | 197 | // Remove $this->staticMethods from $this->methods using array_diff 198 | $this->dataMethods = array_diff($this->methods, $this->staticMethods); 199 | 200 | // Filter the remaining data methods 201 | $this->dataMethods = array_filter($this->dataMethods, function ($method) { 202 | return $method = $method->name; 203 | }); 204 | 205 | // For each method convert method name to snake case and add to data[key => value] 206 | foreach ($this->dataMethods as $method) { 207 | // Convert method name to snake case 208 | $var = Utils::convertToSnakeCase($method->name); 209 | 210 | // Add var method name to data[] 211 | $this->data[$var] = $this->{$method->name}(); 212 | } 213 | } 214 | 215 | /** 216 | * Set Blade Data 217 | * 218 | * Update $this->data with __blade 219 | */ 220 | private function __setBladeData() 221 | { 222 | // Get the data 223 | $debuggerData = $this->data; 224 | 225 | // Loop through each data method 226 | foreach ($this->dataMethods as $dataMethod) { 227 | // Convert the key to snake case to find in $debuggerData 228 | $key = Utils::convertToSnakeCase($dataMethod->name); 229 | 230 | // Save the returned value from the above key 231 | $returned = $debuggerData[$key]; 232 | 233 | // Recreate the key with the method included 234 | $debuggerData[$key] = (object) [ 235 | 'method' => $dataMethod, 236 | 'returned' => $returned 237 | ]; 238 | } 239 | 240 | // Create the final debugger object 241 | $debugger = (object) [ 242 | 'class' => $this->class->getShortName(), 243 | 'tree' => $this->tree, 244 | 'methods' => $this->staticMethods, 245 | 'data' => $debuggerData 246 | ]; 247 | 248 | // Include current debugger data in existing debugger array 249 | $this->incomingData['__blade'][] = $debugger; 250 | 251 | // Set the updated array to $this->data for @debug use 252 | $this->data['__blade'] = $this->incomingData['__blade']; 253 | } 254 | 255 | /** 256 | * Set App Data 257 | * 258 | * Update $this->data with __app 259 | */ 260 | private function __setAppData() 261 | { 262 | if ($this->template === 'app') { 263 | // Save the app data in $this->data['__app'] 264 | $this->data['__app'] = $this->data; 265 | // Esc the function 266 | return; 267 | } 268 | 269 | // Save the app data to this $this->data['__app'] for next Controller 270 | $this->data['__app'] = $this->incomingData['__app']; 271 | 272 | // Include the app data with this current items data 273 | $this->data = array_merge($this->data['__app'], $this->data); 274 | } 275 | 276 | /** 277 | * Set Tree Data 278 | * 279 | * Update $this->data with tree data if required and store existing data 280 | */ 281 | private function __setTreeData() 282 | { 283 | if ($this->tree) { 284 | // Include existing data with this Controller data 285 | $this->data = array_merge($this->incomingData['__store'], $this->data); 286 | } 287 | 288 | // Save updated data to $this->data['__store'] for next Controller 289 | $this->data['__store'] = array_merge($this->incomingData, $this->data); 290 | } 291 | 292 | /** 293 | * Get Template Param 294 | * 295 | * @return string 296 | */ 297 | final public function __getTemplateParam() 298 | { 299 | return ($this->active ? $this->template : false); 300 | } 301 | 302 | /** 303 | * Get Controller Data 304 | * 305 | * @return array 306 | */ 307 | final public function __getData() 308 | { 309 | return ($this->active ? $this->data : []); 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /src/Loader.php: -------------------------------------------------------------------------------- 1 | hierarchy = $hierarchy; 30 | 31 | // Set the default or custom namespace used for Controller files 32 | $this->setNamespace(); 33 | 34 | // Set the path using $this->namespace assuming PSR4 autoloading 35 | $this->setPath(); 36 | 37 | // Return if there are no Controller files 38 | if (!file_exists($this->path)) { 39 | return; 40 | } 41 | 42 | // Set the list of files from the Controller files namespace/path 43 | $this->setListOfFiles(); 44 | 45 | // Set the classes to run from the list of files 46 | $this->setClassesToRun(); 47 | 48 | // Set the aliases for static functions from the list of classes to run 49 | $this->setClassesAlias(); 50 | 51 | // Add the -data body classes for the Blade filter 52 | $this->addBodyDataClasses(); 53 | } 54 | 55 | /** 56 | * Set Namespace 57 | * 58 | * Set the namespace from the filter or use the default 59 | */ 60 | protected function setNamespace() 61 | { 62 | $this->namespace = 63 | (has_filter('sober/controller/namespace') 64 | ? apply_filters('sober/controller/namespace', rtrim($this->namespace)) 65 | : 'App\Controllers'); 66 | } 67 | 68 | /** 69 | * Set Path 70 | * 71 | * Set the path assuming PSR4 autoloading from $this->namespace 72 | */ 73 | protected function setPath() 74 | { 75 | $reflection = new \ReflectionClass($this->namespace .'\App'); 76 | $this->path = dirname($reflection->getFileName()); 77 | } 78 | 79 | /** 80 | * Set File List 81 | * 82 | * Recursively get file list and place into array 83 | */ 84 | protected function setListOfFiles() 85 | { 86 | $this->listOfFiles = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->path)); 87 | } 88 | 89 | /** 90 | * Set Class Instances 91 | * 92 | * Load each Class instance and store in $instances[] 93 | */ 94 | protected function setClassesToRun() 95 | { 96 | foreach ($this->listOfFiles as $filename => $file) { 97 | // Exclude non-PHP files 98 | if (!Utils::isFilePhp($filename)) { 99 | continue; 100 | } 101 | 102 | // Exclude non-Controller classes 103 | if (!Utils::doesFileContain($filename, 'extends Controller')) { 104 | continue; 105 | } 106 | 107 | // Set the classes to run 108 | $this->classesToRun[] = $this->namespace . '\\' . pathinfo($filename, PATHINFO_FILENAME); 109 | } 110 | } 111 | 112 | /** 113 | * Set Class Alias 114 | * 115 | * Remove namespace from static functions 116 | */ 117 | public function setClassesAlias() 118 | { 119 | // Alias each class from $this->classesToRun 120 | foreach ($this->classesToRun as $class) { 121 | class_alias($class, (new \ReflectionClass($class))->getShortName()); 122 | } 123 | } 124 | 125 | /** 126 | * Set Document Classes 127 | * 128 | * Set the classes required for the blade filter to pass on data 129 | * @return array 130 | */ 131 | protected function addBodyDataClasses() 132 | { 133 | add_filter('body_class', function ($body) { 134 | global $wp_query; 135 | 136 | // Get the template hierarchy from WordPress 137 | $templates = $this->hierarchy->getTemplates($wp_query); 138 | 139 | // Reverse the templates returned from $this->hierarchy 140 | $templates = array_reverse($templates); 141 | 142 | // Add app-data to classes array 143 | $classes[] = 'app-data'; 144 | 145 | foreach ($templates as $template) { 146 | // Exclude .blade.php and index.php 147 | if (strpos($template, '.blade.php') || $template === 'index.php') { 148 | continue; 149 | } 150 | 151 | // Exclude index as we use app 152 | if ($template === 'index') { 153 | $template = 'index.php'; 154 | } 155 | 156 | // Replace .php with -data and add to the classes array 157 | $classes[] = basename(str_replace('.php', '-data', $template)); 158 | } 159 | 160 | // Return the new body class list for WordPress 161 | return array_merge($body, $classes); 162 | }); 163 | } 164 | 165 | /** 166 | * Get Classes To Run 167 | * 168 | * @return array 169 | */ 170 | public function getClassesToRun() 171 | { 172 | return $this->classesToRun; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/Module/Acf.php: -------------------------------------------------------------------------------- 1 | setReturnFilter(); 22 | } 23 | 24 | /** 25 | * Set Return Filter 26 | * 27 | * Return filter sober/controller/acf-array 28 | */ 29 | private function setReturnFilter() 30 | { 31 | $this->returnArrayFormat = 32 | (has_filter('sober/controller/acf/array') 33 | ? apply_filters('sober/controller/acf/array', $this->returnArrayFormat) 34 | : false); 35 | } 36 | 37 | /** 38 | * Iterates over array and adds a new snake cased key, with orignial value, for each kebab cased key 39 | * 40 | * Return void 41 | */ 42 | private function recursiveSnakeCase(&$data) { 43 | if(!is_array($data)) 44 | return; 45 | 46 | foreach ($data as $key => $val) { 47 | if (is_array($val)) { 48 | $this->recursiveSnakeCase($val); 49 | } else { 50 | $data[Utils::convertKebabCaseToSnakeCase($key)] = $val; 51 | } 52 | } 53 | } 54 | 55 | /** 56 | * Set Data Return Format 57 | * 58 | * Return object from array if acf/array filter is not set to true 59 | */ 60 | public function setDataReturnFormat() 61 | { 62 | if ($this->returnArrayFormat) { 63 | return; 64 | } 65 | 66 | if ($this->data) { 67 | foreach ($this->data as $key => $item) { 68 | $this->data[$key] = json_decode(json_encode($item)); 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * Set Data Options Page 75 | * 76 | * Set data from the options page 77 | */ 78 | public function setDataOptionsPage() 79 | { 80 | if (!function_exists('acf_add_options_page')) { 81 | return []; 82 | } 83 | 84 | if (get_fields('options')) { 85 | $this->data['acf_options'] = get_fields('options'); 86 | } else { 87 | return []; 88 | } 89 | } 90 | 91 | /** 92 | * Set Data 93 | * 94 | * Set data from passed in field keys 95 | */ 96 | public function setData($acf) 97 | { 98 | $query = get_queried_object(); 99 | 100 | if (!acf_get_valid_post_id($query)) { 101 | return; 102 | } 103 | 104 | if (is_bool($acf)) { 105 | $this->data = get_fields($query); 106 | } 107 | 108 | if (is_string($acf)) { 109 | $this->data = [$acf => get_field($acf, $query)]; 110 | } 111 | 112 | if (is_array($acf)) { 113 | foreach ($acf as $item) { 114 | $this->data[$item] = get_field($item, $query); 115 | } 116 | } 117 | 118 | $this->recursiveSnakeCase($this->data); 119 | } 120 | 121 | /** 122 | * Get Data 123 | * 124 | * Return the data 125 | * @return array 126 | */ 127 | public function getData() 128 | { 129 | return is_array($this->data) ? $this->data : []; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Module/Tree.php: -------------------------------------------------------------------------------- 1 |