├── plugins └── empty ├── webroot ├── js │ └── empty ├── favicon.ico ├── img │ ├── cake.icon.png │ └── cake.power.gif ├── .htaccess ├── index.php └── css │ └── cake.css ├── src ├── View │ ├── Helper │ │ └── empty │ └── AppView.php ├── Model │ ├── Behavior │ │ └── empty │ ├── Entity │ │ ├── User.php │ │ └── Category.php │ └── Table │ │ ├── ArticlesTable.php │ │ ├── UsersTable.php │ │ └── CategoriesTable.php ├── Controller │ ├── Component │ │ └── empty │ ├── PagesController.php │ ├── UsersController.php │ ├── AppController.php │ ├── ArticlesController.php │ └── CategoriesController.php ├── Template │ ├── Element │ │ └── Flash │ │ │ ├── error.ctp │ │ │ ├── success.ctp │ │ │ └── default.ctp │ ├── Articles │ │ ├── view.ctp │ │ ├── edit.ctp │ │ ├── add.ctp │ │ └── index.ctp │ ├── Layout │ │ ├── rss │ │ │ └── default.ctp │ │ ├── ajax.ctp │ │ ├── Email │ │ │ ├── text │ │ │ │ └── default.ctp │ │ │ └── html │ │ │ │ └── default.ctp │ │ ├── error.ctp │ │ └── default.ctp │ ├── Users │ │ ├── login.ctp │ │ ├── add.ctp │ │ ├── view.ctp │ │ └── index.ctp │ ├── Email │ │ ├── text │ │ │ └── default.ctp │ │ └── html │ │ │ └── default.ctp │ ├── Error │ │ ├── error500.ctp │ │ └── error400.ctp │ ├── Categories │ │ ├── add.ctp │ │ ├── edit.ctp │ │ ├── index.ctp │ │ └── view.ctp │ └── Pages │ │ └── home.ctp ├── Shell │ └── ConsoleShell.php └── Console │ └── Installer.php ├── tests ├── TestCase │ ├── View │ │ └── Helper │ │ │ └── empty │ ├── Model │ │ ├── Behavior │ │ │ └── empty │ │ └── Table │ │ │ ├── UsersTableTest.php │ │ │ └── CategoriesTableTest.php │ └── Controller │ │ ├── Component │ │ └── empty │ │ └── CategoriesControllerTest.php ├── bootstrap.php └── Fixture │ ├── UsersFixture.php │ ├── ArticlesFixture.php │ └── CategoriesFixture.php ├── .gitignore ├── .htaccess ├── behat.yml ├── .editorconfig ├── .travis.yml ├── config ├── schema │ ├── sessions.sql │ └── i18n.sql ├── Migrations │ ├── 20150428111734_add_user_id_to_articles.php │ ├── 20150419074519_create_articles.php │ ├── 20150428105014_create_users.php │ └── 20150428102422_create_categories.php ├── bootstrap_cli.php ├── paths.php ├── routes.php ├── bootstrap.php └── app.default.php ├── features ├── bootstrap │ ├── WebContext.php │ ├── CategoriesContext.php │ ├── UsersContext.php │ ├── ArticlesContext.php │ └── FeatureContext.php └── articles.feature ├── index.php ├── bin ├── cake.php ├── cake.bat └── cake ├── blog-tutorial.app.test ├── .gitattributes ├── phpunit.xml.dist ├── LICENSE ├── composer.json └── README.md /plugins/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webroot/js/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/View/Helper/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Model/Behavior/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Controller/Component/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/TestCase/View/Helper/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/TestCase/Model/Behavior/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/TestCase/Controller/Component/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/* 2 | /config/app.php 3 | /tmp/* 4 | /logs/* 5 | -------------------------------------------------------------------------------- /src/Template/Element/Flash/error.ctp: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /src/Template/Element/Flash/success.ctp: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /webroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sizuhiko/cakephp3-bdd-example/HEAD/webroot/favicon.ico -------------------------------------------------------------------------------- /webroot/img/cake.icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sizuhiko/cakephp3-bdd-example/HEAD/webroot/img/cake.icon.png -------------------------------------------------------------------------------- /webroot/img/cake.power.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sizuhiko/cakephp3-bdd-example/HEAD/webroot/img/cake.power.gif -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine on 3 | RewriteRule ^$ webroot/ [L] 4 | RewriteRule (.*) webroot/$1 [L] 5 | -------------------------------------------------------------------------------- /webroot/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine On 3 | RewriteCond %{REQUEST_FILENAME} !-f 4 | RewriteRule ^ index.php [L] 5 | 6 | -------------------------------------------------------------------------------- /src/Template/Element/Flash/default.ctp: -------------------------------------------------------------------------------- 1 | 7 |
8 | -------------------------------------------------------------------------------- /src/Template/Articles/view.ctp: -------------------------------------------------------------------------------- 1 | 2 | 3 |

title) ?>

4 |

body) ?>

5 |

Created: created->format(DATE_RFC850) ?>

-------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Edit Article

4 | Form->create($article); 6 | echo $this->Form->input('title'); 7 | echo $this->Form->input('body', ['rows' => '3']); 8 | echo $this->Form->button(__('Save Article')); 9 | echo $this->Form->end(); 10 | ?> -------------------------------------------------------------------------------- /src/Template/Layout/rss/default.ctp: -------------------------------------------------------------------------------- 1 | fetch('title'); 7 | endif; 8 | 9 | echo $this->Rss->document( 10 | $this->Rss->channel( 11 | array(), $channel, $this->fetch('content') 12 | ) 13 | ); 14 | ?> 15 | -------------------------------------------------------------------------------- /tests/Fixture/UsersFixture.php: -------------------------------------------------------------------------------- 1 | 'users']; 14 | 15 | /** 16 | * Records 17 | * 18 | * @var array 19 | */ 20 | public $records = []; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /tests/Fixture/ArticlesFixture.php: -------------------------------------------------------------------------------- 1 | 'articles']; 14 | 15 | /** 16 | * Records 17 | * 18 | * @var array 19 | */ 20 | public $records = []; 21 | } 22 | -------------------------------------------------------------------------------- /behat.yml: -------------------------------------------------------------------------------- 1 | # behat.yml 2 | default: 3 | # ... 4 | extensions: 5 | Behat\MinkExtension: 6 | base_url: 'http://blog-tutorial.app.test/' 7 | sessions: 8 | default: 9 | goutte: ~ 10 | suites: 11 | default: 12 | contexts: 13 | - FeatureContext 14 | - WebContext 15 | - ArticlesContext 16 | - UsersContext 17 | - CategoriesContext 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.bat] 14 | end_of_line = crlf 15 | 16 | [*.yml] 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /tests/Fixture/CategoriesFixture.php: -------------------------------------------------------------------------------- 1 | 'categories']; 14 | 15 | /** 16 | * Records 17 | * 18 | * @var array 19 | */ 20 | public $records = []; 21 | } 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | 5 | php: 6 | - 5.4 7 | - 5.5 8 | - 5.6 9 | 10 | before_script: 11 | - sh -c "composer require 'cakephp/cakephp-codesniffer:dev-master'" 12 | - phpenv rehash 13 | 14 | script: 15 | - sh -c "vendor/bin/phpcs -p --extensions=php --standard=vendor/cakephp/cakephp-codesniffer/CakePHP ./src ./tests ./config ./webroot" 16 | 17 | notifications: 18 | email: false 19 | -------------------------------------------------------------------------------- /src/Template/Articles/add.ctp: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Add Article

4 | Form->create($article); 6 | // just added the categories input 7 | echo $this->Form->input('categories'); 8 | echo $this->Form->input('title'); 9 | echo $this->Form->input('body', ['rows' => '3']); 10 | echo $this->Form->button(__('Save Article')); 11 | echo $this->Form->end(); 12 | ?> -------------------------------------------------------------------------------- /src/Template/Users/login.ctp: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | Flash->render('auth') ?> 5 | Form->create() ?> 6 |
7 | 8 | Form->input('username') ?> 9 | Form->input('password') ?> 10 |
11 | Form->button(__('Login')); ?> 12 | Form->end() ?> 13 |
-------------------------------------------------------------------------------- /src/Model/Entity/User.php: -------------------------------------------------------------------------------- 1 | true]; 20 | 21 | protected function _setPassword($password) 22 | { 23 | return (new DefaultPasswordHasher)->hash($password); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /config/schema/sessions.sql: -------------------------------------------------------------------------------- 1 | # $Id$ 2 | # 3 | # Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) 4 | # 1785 E. Sahara Avenue, Suite 490-204 5 | # Las Vegas, Nevada 89104 6 | # 7 | # Licensed under The MIT License 8 | # For full copyright and license information, please see the LICENSE.txt 9 | # Redistributions of files must retain the above copyright notice. 10 | # MIT License (http://www.opensource.org/licenses/mit-license.php) 11 | 12 | CREATE TABLE sessions ( 13 | id varchar(40) NOT NULL default '', 14 | data text, 15 | expires INT(11) NOT NULL, 16 | PRIMARY KEY (id) 17 | ); 18 | -------------------------------------------------------------------------------- /src/Model/Entity/Category.php: -------------------------------------------------------------------------------- 1 | true, 19 | 'lft' => true, 20 | 'rght' => true, 21 | 'name' => true, 22 | 'description' => true, 23 | 'parent_category' => true, 24 | 'articles' => true, 25 | 'child_categories' => true, 26 | ]; 27 | } 28 | -------------------------------------------------------------------------------- /features/bootstrap/WebContext.php: -------------------------------------------------------------------------------- 1 | getPathTo($path)); 11 | } 12 | 13 | private function getPathTo($path) 14 | { 15 | switch ($path) { 16 | case 'TopPage': return Router::url(['controller' => 'articles', 'action' => 'index']); 17 | case 'トップページ': return Router::url(['controller' => 'articles', 'action' => 'index']); 18 | default: return $path; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Template/Email/text/default.ctp: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 16 | fetch('content') ?> 17 | -------------------------------------------------------------------------------- /src/Template/Layout/Email/text/default.ctp: -------------------------------------------------------------------------------- 1 | 16 | fetch('content') ?> 17 | -------------------------------------------------------------------------------- /src/Template/Users/add.ctp: -------------------------------------------------------------------------------- 1 |
2 |

3 | 6 |
7 |
8 | Form->create($user); ?> 9 |
10 | 11 | Form->input('username'); 13 | echo $this->Form->input('password'); 14 | echo $this->Form->input('role', [ 15 | 'options' => ['admin' => 'Admin', 'author' => 'Author'] 16 | ]); 17 | ?> 18 |
19 | Form->button(__('Submit')) ?> 20 | Form->end() ?> 21 |
22 | -------------------------------------------------------------------------------- /src/Template/Email/html/default.ctp: -------------------------------------------------------------------------------- 1 | 16 | ' . $line . "

\n"; 21 | endforeach; 22 | ?> 23 | -------------------------------------------------------------------------------- /features/bootstrap/CategoriesContext.php: -------------------------------------------------------------------------------- 1 | getHash(); 16 | Fabricate::create('Categories', count($categories), function($data, $world) use($categories) { 17 | $index = $world->sequence('index', 0); 18 | return [ 19 | 'parent_id' => null, 20 | 'lft' => null, 21 | 'rght' => null, 22 | 'name' => $categories[$index]['Name'], 23 | ]; 24 | }); 25 | 26 | } 27 | } -------------------------------------------------------------------------------- /src/Model/Table/ArticlesTable.php: -------------------------------------------------------------------------------- 1 | addBehavior('Timestamp'); 14 | $this->belongsTo('Categories', [ 15 | 'foreignKey' => 'category_id', 16 | ]); 17 | } 18 | 19 | public function validationDefault(Validator $validator) 20 | { 21 | $validator 22 | ->notEmpty('title') 23 | ->notEmpty('body'); 24 | 25 | return $validator; 26 | } 27 | 28 | public function isOwnedBy($articleId, $userId) 29 | { 30 | return $this->exists(['id' => $articleId, 'user_id' => $userId]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /bin/cake.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php -q 2 | 16 | 17 | 18 | 19 | <?= $this->fetch('title') ?> 20 | 21 | 22 | fetch('content') ?> 23 | 24 | 25 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Define the line ending behavior of the different file extensions 2 | # Set default behaviour, in case users don't have core.autocrlf set. 3 | * text=auto 4 | * text eol=lf 5 | 6 | # Explicitly declare text files we want to always be normalized and converted 7 | # to native line endings on checkout. 8 | *.php text 9 | *.default text 10 | *.ctp text 11 | *.sql text 12 | *.md text 13 | *.po text 14 | *.js text 15 | *.css text 16 | *.ini text 17 | *.properties text 18 | *.txt text 19 | *.xml text 20 | *.yml text 21 | .htaccess text 22 | 23 | # Declare files that will always have CRLF line endings on checkout. 24 | *.bat eol=crlf 25 | 26 | # Declare files that will always have LF line endings on checkout. 27 | *.pem eol=lf 28 | 29 | # Denote all files that are truly binary and should not be modified. 30 | *.png binary 31 | *.jpg binary 32 | *.gif binary 33 | *.ico binary 34 | *.mo binary 35 | *.pdf binary 36 | *.phar binary 37 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ./tests/TestCase 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/View/AppView.php: -------------------------------------------------------------------------------- 1 | loadHelper('Html');` 29 | * 30 | * @return void 31 | */ 32 | public function initialize() 33 | { 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /config/schema/i18n.sql: -------------------------------------------------------------------------------- 1 | # $Id$ 2 | # 3 | # Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) 4 | # 5 | # Licensed under The MIT License 6 | # For full copyright and license information, please see the LICENSE.txt 7 | # Redistributions of files must retain the above copyright notice. 8 | # MIT License (http://www.opensource.org/licenses/mit-license.php) 9 | 10 | CREATE TABLE i18n ( 11 | id int(10) NOT NULL auto_increment, 12 | locale varchar(6) NOT NULL, 13 | model varchar(255) NOT NULL, 14 | foreign_key int(10) NOT NULL, 15 | field varchar(255) NOT NULL, 16 | content mediumtext, 17 | PRIMARY KEY (id), 18 | # UNIQUE INDEX I18N_LOCALE_FIELD(locale, model, foreign_key, field), 19 | # INDEX I18N_LOCALE_ROW(locale, model, foreign_key), 20 | # INDEX I18N_LOCALE_MODEL(locale, model), 21 | # INDEX I18N_FIELD(model, foreign_key, field), 22 | # INDEX I18N_ROW(model, foreign_key), 23 | INDEX locale (locale), 24 | INDEX model (model), 25 | INDEX row_id (foreign_key), 26 | INDEX field (field) 27 | ); -------------------------------------------------------------------------------- /config/Migrations/20150428111734_add_user_id_to_articles.php: -------------------------------------------------------------------------------- 1 | table('articles'); 26 | $articles->addColumn('user_id', 'integer', ['after' => 'category_id']) 27 | ->save(); 28 | } 29 | 30 | /** 31 | * Migrate Down. 32 | */ 33 | public function down() 34 | { 35 | $articles = $this->table('articles'); 36 | $articles->removeColumn('user_id') 37 | ->save(); 38 | } 39 | } -------------------------------------------------------------------------------- /config/Migrations/20150419074519_create_articles.php: -------------------------------------------------------------------------------- 1 | table('articles'); 26 | $table->addColumn('title', 'string', ['limit' => 50]) 27 | ->addColumn('body', 'text') 28 | ->addColumn('created', 'datetime') 29 | ->addColumn('modified', 'datetime') 30 | ->create(); 31 | } 32 | 33 | /** 34 | * Migrate Down. 35 | */ 36 | public function down() 37 | { 38 | $this->dropTable('articles'); 39 | } 40 | } -------------------------------------------------------------------------------- /src/Template/Error/error500.ctp: -------------------------------------------------------------------------------- 1 | layout = 'dev_error'; 7 | 8 | $this->assign('title', $message); 9 | $this->assign('templateName', 'error500.ctp'); 10 | 11 | $this->start('file'); 12 | ?> 13 | queryString)) : ?> 14 |

15 | SQL Query: 16 | queryString) ?> 17 |

18 | 19 | params)) : ?> 20 | SQL Query Params: 21 | params) ?> 22 | 23 | element('auto_table_warning'); 25 | 26 | if (extension_loaded('xdebug')): 27 | xdebug_print_function_stack(); 28 | endif; 29 | 30 | $this->end(); 31 | endif; 32 | ?> 33 |

34 |

35 | : 36 | 37 |

38 | -------------------------------------------------------------------------------- /src/Template/Categories/add.ctp: -------------------------------------------------------------------------------- 1 |
2 |

3 |
    4 |
  • Html->link(__('List Categories'), ['action' => 'index']) ?>
  • 5 |
  • Html->link(__('List Articles'), ['controller' => 'Articles', 'action' => 'index']) ?>
  • 6 |
  • Html->link(__('New Article'), ['controller' => 'Articles', 'action' => 'add']) ?>
  • 7 |
8 |
9 |
10 | Form->create($category); ?> 11 |
12 | 13 | Form->input('parent_id'); 15 | echo $this->Form->input('lft'); 16 | echo $this->Form->input('rght'); 17 | echo $this->Form->input('name'); 18 | echo $this->Form->input('description'); 19 | ?> 20 |
21 | Form->button(__('Submit')) ?> 22 | Form->end() ?> 23 |
24 | -------------------------------------------------------------------------------- /bin/cake.bat: -------------------------------------------------------------------------------- 1 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 2 | :: 3 | :: Bake is a shell script for running CakePHP bake script 4 | :: 5 | :: CakePHP(tm) : Rapid Development Framework (http://cakephp.org) 6 | :: Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) 7 | :: 8 | :: Licensed under The MIT License 9 | :: Redistributions of files must retain the above copyright notice. 10 | :: 11 | :: @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) 12 | :: @link http://cakephp.org CakePHP(tm) Project 13 | :: @since 2.0.0 14 | :: @license http://www.opensource.org/licenses/mit-license.php MIT License 15 | :: 16 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 17 | 18 | :: In order for this script to work as intended, the cake\console\ folder must be in your PATH 19 | 20 | @echo. 21 | @echo off 22 | 23 | SET app=%0 24 | SET lib=%~dp0 25 | 26 | php "%lib%cake.php" %* 27 | 28 | echo. 29 | 30 | exit /B %ERRORLEVEL% 31 | -------------------------------------------------------------------------------- /src/Template/Error/error400.ctp: -------------------------------------------------------------------------------- 1 | layout = 'dev_error'; 6 | 7 | $this->assign('title', $message); 8 | $this->assign('templateName', 'error400.ctp'); 9 | 10 | $this->start('file'); 11 | ?> 12 | queryString)) : ?> 13 |

14 | SQL Query: 15 | queryString) ?> 16 |

17 | 18 | params)) : ?> 19 | SQL Query Params: 20 | params) ?> 21 | 22 | element('auto_table_warning') ?> 23 | end(); 29 | endif; 30 | ?> 31 |

32 |

33 | : 34 | '{$url}'" 37 | ) ?> 38 |

39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Kenichiro Kishida 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 | 23 | -------------------------------------------------------------------------------- /src/Template/Articles/index.ctp: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Blog articles

4 |

Html->link('Add Article', ['action' => 'add']) ?>

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 24 | 32 | 33 | 34 | 35 |
IdTitleCreatedActions
id ?> 19 | Html->link($article->title, ['action' => 'view', $article->id]) ?> 20 | 22 | created->format(DATE_RFC850) ?> 23 | 25 | Form->postLink( 26 | 'Delete', 27 | ['action' => 'delete', $article->id], 28 | ['confirm' => 'Are you sure?']) 29 | ?> 30 | Html->link('Edit', ['action' => 'edit', $article->id]) ?> 31 |
-------------------------------------------------------------------------------- /config/Migrations/20150428105014_create_users.php: -------------------------------------------------------------------------------- 1 | table('users'); 26 | $users->addColumn('username', 'string', ['limit' => 50]) 27 | ->addColumn('password', 'string', ['limit' => 255]) 28 | ->addColumn('role', 'string', ['limit' => 20]) 29 | ->addColumn('created', 'datetime', ['null' => true, 'default' => null]) 30 | ->addColumn('modified', 'datetime', ['null' => true, 'default' => null]) 31 | ->create(); 32 | } 33 | 34 | /** 35 | * Migrate Down. 36 | */ 37 | public function down() 38 | { 39 | $this->dropTable('users'); 40 | } 41 | } -------------------------------------------------------------------------------- /config/bootstrap_cli.php: -------------------------------------------------------------------------------- 1 | getHash(); 16 | Fabricate::create('Users', count($users), function($data, $world) use($users) { 17 | $index = $world->sequence('index', 0); 18 | return [ 19 | 'username' => $users[$index]['Username'], 20 | 'password' => $users[$index]['Password'], 21 | 'firstname' => $users[$index]['FirstName'], 22 | 'lastname' => $users[$index]['LastName']]; 23 | }); 24 | } 25 | 26 | /** 27 | * @Given I login :username :password 28 | */ 29 | public function iLogin($username, $password) 30 | { 31 | $page = $this->getSession()->getPage(); 32 | 33 | $page->findField('Username')->setValue($username); 34 | $page->findField('Password')->setValue($password); 35 | $page->findButton("Login")->press(); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /tests/TestCase/Model/Table/UsersTableTest.php: -------------------------------------------------------------------------------- 1 | 'app.users' 21 | ]; 22 | 23 | /** 24 | * setUp method 25 | * 26 | * @return void 27 | */ 28 | public function setUp() 29 | { 30 | parent::setUp(); 31 | $config = TableRegistry::exists('Users') ? [] : ['className' => 'App\Model\Table\UsersTable']; 32 | $this->Users = TableRegistry::get('Users', $config); 33 | } 34 | 35 | /** 36 | * tearDown method 37 | * 38 | * @return void 39 | */ 40 | public function tearDown() 41 | { 42 | unset($this->Users); 43 | 44 | parent::tearDown(); 45 | } 46 | 47 | /** 48 | * Test validationDefault method 49 | * 50 | * @return void 51 | */ 52 | public function testValidationDefault() 53 | { 54 | $this->markTestIncomplete('Not implemented yet.'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Template/Categories/edit.ctp: -------------------------------------------------------------------------------- 1 |
2 |

3 |
    4 |
  • Form->postLink( 5 | __('Delete'), 6 | ['action' => 'delete', $category->id], 7 | ['confirm' => __('Are you sure you want to delete # {0}?', $category->id)] 8 | ) 9 | ?>
  • 10 |
  • Html->link(__('List Categories'), ['action' => 'index']) ?>
  • 11 |
  • Html->link(__('List Articles'), ['controller' => 'Articles', 'action' => 'index']) ?>
  • 12 |
  • Html->link(__('New Article'), ['controller' => 'Articles', 'action' => 'add']) ?>
  • 13 |
14 |
15 |
16 | Form->create($category); ?> 17 |
18 | 19 | Form->input('parent_id'); 21 | echo $this->Form->input('lft'); 22 | echo $this->Form->input('rght'); 23 | echo $this->Form->input('name'); 24 | echo $this->Form->input('description'); 25 | ?> 26 |
27 | Form->button(__('Submit')) ?> 28 | Form->end() ?> 29 |
30 | -------------------------------------------------------------------------------- /webroot/index.php: -------------------------------------------------------------------------------- 1 | dispatch( 35 | Request::createFromGlobals(), 36 | new Response() 37 | ); 38 | -------------------------------------------------------------------------------- /bin/cake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ################################################################################ 3 | # 4 | # Bake is a shell script for running CakePHP bake script 5 | # 6 | # CakePHP(tm) : Rapid Development Framework (http://cakephp.org) 7 | # Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) 8 | # 9 | # Licensed under The MIT License 10 | # For full copyright and license information, please see the LICENSE.txt 11 | # Redistributions of files must retain the above copyright notice. 12 | # 13 | # @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) 14 | # @link http://cakephp.org CakePHP(tm) Project 15 | # @since 1.2.0 16 | # @license http://www.opensource.org/licenses/mit-license.php MIT License 17 | # 18 | ################################################################################ 19 | 20 | # Canonicalize by following every symlink of the given name recursively 21 | canonicalize() { 22 | NAME="$1" 23 | if [ -f "$NAME" ] 24 | then 25 | DIR=$(dirname -- "$NAME") 26 | NAME=$(cd -P "$DIR" > /dev/null && pwd -P)/$(basename -- "$NAME") 27 | fi 28 | while [ -h "$NAME" ]; do 29 | DIR=$(dirname -- "$NAME") 30 | SYM=$(readlink "$NAME") 31 | NAME=$(cd "$DIR" > /dev/null && cd $(dirname -- "$SYM") > /dev/null && pwd)/$(basename -- "$SYM") 32 | done 33 | echo "$NAME" 34 | } 35 | 36 | CONSOLE=$(dirname -- "$(canonicalize "$0")") 37 | APP=$(dirname "$CONSOLE") 38 | 39 | exec php "$CONSOLE"/cake.php "$@" 40 | exit 41 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cakephp/app", 3 | "description": "CakePHP skeleton app", 4 | "homepage": "http://cakephp.org", 5 | "type": "project", 6 | "license": "MIT", 7 | "require": { 8 | "php": ">=5.4.16", 9 | "cakephp/cakephp": "~3.0", 10 | "mobiledetect/mobiledetectlib": "2.*", 11 | "cakephp/migrations": "~1.0", 12 | "cakephp/plugin-installer": "*", 13 | "vlucas/phpdotenv": "~1.1@dev" 14 | }, 15 | "require-dev": { 16 | "psy/psysh": "@stable", 17 | "cakephp/debug_kit": "~3.0", 18 | "cakephp/bake": "~1.0", 19 | "behat/mink-extension": "~2.0@dev", 20 | "behat/mink-goutte-driver": "~1.1@dev", 21 | "sizuhiko/cake_fabricate": "dev-master", 22 | "phpunit/phpunit": "~4.8@dev" 23 | }, 24 | "suggest": { 25 | "cakephp/cakephp-codesniffer": "Allows to check the code against the coding standards used in CakePHP." 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "App\\": "src" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "App\\Test\\": "tests", 35 | "Cake\\Test\\": "./vendor/cakephp/cakephp/tests" 36 | } 37 | }, 38 | "scripts": { 39 | "post-install-cmd": "App\\Console\\Installer::postInstall", 40 | "post-autoload-dump": "Cake\\Composer\\Installer\\PluginInstaller::postAutoloadDump" 41 | }, 42 | "minimum-stability": "dev", 43 | "prefer-stable": true 44 | } 45 | -------------------------------------------------------------------------------- /tests/TestCase/Controller/CategoriesControllerTest.php: -------------------------------------------------------------------------------- 1 | 'app.categories', 20 | 'Articles' => 'app.articles' 21 | ]; 22 | 23 | /** 24 | * Test index method 25 | * 26 | * @return void 27 | */ 28 | public function testIndex() 29 | { 30 | $this->markTestIncomplete('Not implemented yet.'); 31 | } 32 | 33 | /** 34 | * Test view method 35 | * 36 | * @return void 37 | */ 38 | public function testView() 39 | { 40 | $this->markTestIncomplete('Not implemented yet.'); 41 | } 42 | 43 | /** 44 | * Test add method 45 | * 46 | * @return void 47 | */ 48 | public function testAdd() 49 | { 50 | $this->markTestIncomplete('Not implemented yet.'); 51 | } 52 | 53 | /** 54 | * Test edit method 55 | * 56 | * @return void 57 | */ 58 | public function testEdit() 59 | { 60 | $this->markTestIncomplete('Not implemented yet.'); 61 | } 62 | 63 | /** 64 | * Test delete method 65 | * 66 | * @return void 67 | */ 68 | public function testDelete() 69 | { 70 | $this->markTestIncomplete('Not implemented yet.'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Template/Users/view.ctp: -------------------------------------------------------------------------------- 1 |
2 |

3 |
    4 |
  • Html->link(__('Edit User'), ['action' => 'edit', $user->id]) ?>
  • 5 |
  • Form->postLink(__('Delete User'), ['action' => 'delete', $user->id], ['confirm' => __('Are you sure you want to delete # {0}?', $user->id)]) ?>
  • 6 |
  • Html->link(__('List Users'), ['action' => 'index']) ?>
  • 7 |
  • Html->link(__('New User'), ['action' => 'add']) ?>
  • 8 |
9 |
10 |
11 |

id) ?>

12 |
13 |
14 |
15 |

username) ?>

16 |
17 |

password) ?>

18 |
19 |

role) ?>

20 |
21 |
22 |
23 |

Number->format($user->id) ?>

24 |
25 |
26 |
27 |

created) ?>

28 |
29 |

modified) ?>

30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /features/articles.feature: -------------------------------------------------------------------------------- 1 | # language: en 2 | Feature: 3 | In order to tell the masses what's on my mind 4 | As a user 5 | I want to read articles on the site 6 | 7 | Background: 8 | Given there is a post: 9 | | Title | Body | 10 | | The title | This is the post body. | 11 | | A title once again | And the post body follows. | 12 | | Title strikes back | This is really exciting! Not. | 13 | And there is a user: 14 | | Username | Password | FirstName | LastName | 15 | | alice | ecila | Alice | Smith | 16 | | bob | obo | Bob | Johnson | 17 | And there is a category: 18 | | Name | 19 | | Events | 20 | | Computers | 21 | | Foods | 22 | 23 | Scenario: Show articles 24 | When I am on "TopPage" 25 | Then I should see "The title" 26 | And I should see "A title once again" 27 | And I should see "Title strikes back" 28 | 29 | Scenario: Show the article 30 | Given I am on "TopPage" 31 | When I follow "A title once again" 32 | Then I should see "And the post body follows." 33 | 34 | Scenario: Add new article 35 | Given I am on "TopPage" 36 | And I follow "Add" 37 | And I login "bob" "obo" 38 | When I post article form : 39 | | Label | Value | 40 | | Categories | Events | 41 | | Title | Today is Party | 42 | | Body | From 19:30 with Alice | 43 | And I should see "Your article has been saved." 44 | And I should see "Today is party" 45 | 46 | Scenario: Remove article 47 | Given I am on "TopPage" 48 | When I delete article "Title strikes back" 49 | Then I should not see "Title strikes back" 50 | -------------------------------------------------------------------------------- /src/Model/Table/UsersTable.php: -------------------------------------------------------------------------------- 1 | table('users'); 25 | $this->displayField('id'); 26 | $this->primaryKey('id'); 27 | $this->addBehavior('Timestamp'); 28 | } 29 | 30 | /** 31 | * Default validation rules. 32 | * 33 | * @param \Cake\Validation\Validator $validator Validator instance. 34 | * @return \Cake\Validation\Validator 35 | */ 36 | public function validationDefault(Validator $validator) 37 | { 38 | return $validator 39 | ->notEmpty('username', 'A username is required') 40 | ->notEmpty('password', 'A password is required') 41 | ->notEmpty('role', 'A role is required') 42 | ->add('role', 'inList', [ 43 | 'rule' => ['inList', ['admin', 'author']], 44 | 'message' => 'Please enter a valid role' 45 | ]); 46 | } 47 | 48 | /** 49 | * Returns a rules checker object that will be used for validating 50 | * application integrity. 51 | * 52 | * @param \Cake\ORM\RulesChecker $rules The rules object to be modified. 53 | * @return \Cake\ORM\RulesChecker 54 | */ 55 | public function buildRules(RulesChecker $rules) 56 | { 57 | $rules->add($rules->isUnique(['username'])); 58 | return $rules; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/TestCase/Model/Table/CategoriesTableTest.php: -------------------------------------------------------------------------------- 1 | 'app.categories', 21 | 'Articles' => 'app.articles' 22 | ]; 23 | 24 | /** 25 | * setUp method 26 | * 27 | * @return void 28 | */ 29 | public function setUp() 30 | { 31 | parent::setUp(); 32 | $config = TableRegistry::exists('Categories') ? [] : ['className' => 'App\Model\Table\CategoriesTable']; 33 | $this->Categories = TableRegistry::get('Categories', $config); 34 | } 35 | 36 | /** 37 | * tearDown method 38 | * 39 | * @return void 40 | */ 41 | public function tearDown() 42 | { 43 | unset($this->Categories); 44 | 45 | parent::tearDown(); 46 | } 47 | 48 | /** 49 | * Test initialize method 50 | * 51 | * @return void 52 | */ 53 | public function testInitialize() 54 | { 55 | $this->markTestIncomplete('Not implemented yet.'); 56 | } 57 | 58 | /** 59 | * Test validationDefault method 60 | * 61 | * @return void 62 | */ 63 | public function testValidationDefault() 64 | { 65 | $this->markTestIncomplete('Not implemented yet.'); 66 | } 67 | 68 | /** 69 | * Test buildRules method 70 | * 71 | * @return void 72 | */ 73 | public function testBuildRules() 74 | { 75 | $this->markTestIncomplete('Not implemented yet.'); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /config/Migrations/20150428102422_create_categories.php: -------------------------------------------------------------------------------- 1 | table('categories'); 26 | $categories->addColumn('parent_id', 'integer', ['null' => true, 'default' => null]) 27 | ->addColumn('lft', 'integer', ['null' => true, 'default' => null]) 28 | ->addColumn('rght', 'integer', ['null' => true, 'default' => null]) 29 | ->addColumn('name', 'string', ['limit' => 255]) 30 | ->addColumn('description', 'string', ['limit' => 255, 'null' => true, 'default' => null]) 31 | ->addColumn('created', 'datetime') 32 | ->addColumn('modified', 'datetime', ['null' => true, 'default' => null]) 33 | ->create(); 34 | 35 | $articles = $this->table('articles'); 36 | $articles->changeColumn('body', 'text', ['null' => true, 'default' => null]) 37 | ->addColumn('category_id', 'integer', ['null' => true, 'default' => null, 'after' => 'body']) 38 | ->save(); 39 | } 40 | 41 | /** 42 | * Migrate Down. 43 | */ 44 | public function down() 45 | { 46 | $articles = $this->table('articles'); 47 | $articles->changeColumn('body', 'text') 48 | ->removeColumn('category_id') 49 | ->save(); 50 | 51 | $this->dropTable('categories'); 52 | } 53 | } -------------------------------------------------------------------------------- /src/Template/Layout/error.ctp: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | Html->charset() ?> 22 | 23 | <?= $cakeDescription ?>: 24 | <?= $this->fetch('title') ?> 25 | 26 | Html->meta('icon') ?> 27 | 28 | Html->css('base.css') ?> 29 | Html->css('cake.css') ?> 30 | 31 | fetch('meta') ?> 32 | fetch('css') ?> 33 | fetch('script') ?> 34 | 35 | 36 |
37 | 40 |
41 | Flash->render() ?> 42 | 43 | fetch('content') ?> 44 |
45 | 53 |
54 | 55 | 56 | -------------------------------------------------------------------------------- /src/Template/Layout/default.ctp: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | Html->charset() ?> 22 | 23 | 24 | <?= $cakeDescription ?>: 25 | <?= $this->fetch('title') ?> 26 | 27 | Html->meta('icon') ?> 28 | 29 | Html->css('base.css') ?> 30 | Html->css('cake.css') ?> 31 | 32 | fetch('meta') ?> 33 | fetch('css') ?> 34 | fetch('script') ?> 35 | 36 | 37 |
38 |
39 | fetch('title') ?> 40 |
41 |
42 | Documentation 43 | API 44 |
45 |
46 |
47 | 48 |
49 | Flash->render() ?> 50 | 51 |
52 | fetch('content') ?> 53 |
54 |
55 |
56 |
57 |
58 | 59 | 60 | -------------------------------------------------------------------------------- /features/bootstrap/ArticlesContext.php: -------------------------------------------------------------------------------- 1 | getHash(); 17 | Fabricate::create('Articles', count($posts), function($data, $world) use($posts) { 18 | $index = $world->sequence('index', 0); 19 | return ['title'=>$posts[$index]['Title'], 'body'=>$posts[$index]['Body']]; 20 | }); 21 | } 22 | 23 | /** 24 | * @When I post article form : 25 | */ 26 | public function iPostArticleForm(TableNode $table) 27 | { 28 | $hash = $table->getHash(); 29 | $page = $this->getSession()->getPage(); 30 | 31 | foreach ($hash as $field) { 32 | $element = $page->findField($field['Label']); 33 | if ('select' == $element->getTagName()) { 34 | $element->selectOption($field['Value']); 35 | } else { 36 | $element->setValue($field['Value']); 37 | } 38 | } 39 | $page->findButton("Save Article")->press(); 40 | } 41 | 42 | /** 43 | * @When I delete article :title 44 | */ 45 | public function iDeleteArticle($title) 46 | { 47 | $page = $this->getSession()->getPage(); 48 | $table = $page->find('css', 'table#articles'); 49 | foreach ($table->findAll('css', 'tr') as $tr) { 50 | if (!$tr->has('css', 'td')) { 51 | continue; // skip title row 52 | } 53 | if ($tr->findAll('css', 'td')[1]->getText() == $title) { 54 | $tr->find('css', 'form')->submit(); // submit delete form 55 | return; 56 | } 57 | } 58 | throw new ElementNotFoundException($this->getSession(), 'article title', $title, null); 59 | } 60 | } -------------------------------------------------------------------------------- /src/Template/Users/index.ctp: -------------------------------------------------------------------------------- 1 |
2 |

3 |
    4 |
  • Html->link(__('New User'), ['action' => 'add']) ?>
  • 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 | 34 | 35 | 36 | 37 | 38 |
Paginator->sort('id') ?>Paginator->sort('username') ?>Paginator->sort('password') ?>Paginator->sort('role') ?>Paginator->sort('created') ?>Paginator->sort('modified') ?>
Number->format($user->id) ?>username) ?>password) ?>role) ?>created) ?>modified) ?> 30 | Html->link(__('View'), ['action' => 'view', $user->id]) ?> 31 | Html->link(__('Edit'), ['action' => 'edit', $user->id]) ?> 32 | Form->postLink(__('Delete'), ['action' => 'delete', $user->id], ['confirm' => __('Are you sure you want to delete # {0}?', $user->id)]) ?> 33 |
39 |
40 |
    41 | Paginator->prev('< ' . __('previous')) ?> 42 | Paginator->numbers() ?> 43 | Paginator->next(__('next') . ' >') ?> 44 |
45 |

Paginator->counter() ?>

46 |
47 |
48 | -------------------------------------------------------------------------------- /src/Controller/PagesController.php: -------------------------------------------------------------------------------- 1 | redirect('/'); 45 | } 46 | $page = $subpage = null; 47 | 48 | if (!empty($path[0])) { 49 | $page = $path[0]; 50 | } 51 | if (!empty($path[1])) { 52 | $subpage = $path[1]; 53 | } 54 | $this->set(compact('page', 'subpage')); 55 | 56 | try { 57 | $this->render(implode('/', $path)); 58 | } catch (MissingTemplateException $e) { 59 | if (Configure::read('debug')) { 60 | throw $e; 61 | } 62 | throw new NotFoundException(); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Controller/UsersController.php: -------------------------------------------------------------------------------- 1 | Auth->allow(['add', 'logout']); 20 | } 21 | 22 | public function login() 23 | { 24 | if ($this->request->is('post')) { 25 | $user = $this->Auth->identify(); 26 | if ($user) { 27 | $this->Auth->setUser($user); 28 | return $this->redirect($this->Auth->redirectUrl()); 29 | } 30 | $this->Flash->error(__('Invalid username or password, try again')); 31 | } 32 | } 33 | 34 | public function logout() 35 | { 36 | return $this->redirect($this->Auth->logout()); 37 | } 38 | 39 | public function index() 40 | { 41 | $this->set('users', $this->paginate($this->Users)); 42 | $this->set('_serialize', ['users']); 43 | } 44 | 45 | public function view($id) 46 | { 47 | if (!$id) { 48 | throw new NotFoundException(__('Invalid user')); 49 | } 50 | 51 | $user = $this->Users->get($id); 52 | $this->set(compact('user')); 53 | } 54 | 55 | public function add() 56 | { 57 | $user = $this->Users->newEntity(); 58 | if ($this->request->is('post')) { 59 | $user = $this->Users->patchEntity($user, $this->request->data); 60 | if ($this->Users->save($user)) { 61 | $this->Flash->success(__('The user has been saved.')); 62 | return $this->redirect(['action' => 'add']); 63 | } 64 | $this->Flash->error(__('Unable to add the user.')); 65 | } 66 | $this->set('user', $user); 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /src/Controller/AppController.php: -------------------------------------------------------------------------------- 1 | loadComponent('Flash'); 41 | $this->loadComponent('Auth', [ 42 | 'authorize' => ['Controller'], // Added this line 43 | 'loginRedirect' => [ 44 | 'controller' => 'Articles', 45 | 'action' => 'index' 46 | ], 47 | 'logoutRedirect' => [ 48 | 'controller' => 'Pages', 49 | 'action' => 'display', 50 | 'home' 51 | ] 52 | ]); 53 | } 54 | 55 | public function beforeFilter(Event $event) 56 | { 57 | $this->Auth->allow(['index', 'view', 'display']); 58 | } 59 | 60 | public function isAuthorized($user) 61 | { 62 | // Admin can access every action 63 | if (isset($user['role']) && $user['role'] === 'admin') { 64 | return true; 65 | } 66 | 67 | // Default deny 68 | return false; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Model/Table/CategoriesTable.php: -------------------------------------------------------------------------------- 1 | table('categories'); 25 | $this->displayField('name'); 26 | $this->primaryKey('id'); 27 | $this->addBehavior('Timestamp'); 28 | $this->addBehavior('Tree'); 29 | $this->belongsTo('ParentCategories', [ 30 | 'className' => 'Categories', 31 | 'foreignKey' => 'parent_id' 32 | ]); 33 | $this->hasMany('Articles', [ 34 | 'foreignKey' => 'category_id' 35 | ]); 36 | $this->hasMany('ChildCategories', [ 37 | 'className' => 'Categories', 38 | 'foreignKey' => 'parent_id' 39 | ]); 40 | } 41 | 42 | /** 43 | * Default validation rules. 44 | * 45 | * @param \Cake\Validation\Validator $validator Validator instance. 46 | * @return \Cake\Validation\Validator 47 | */ 48 | public function validationDefault(Validator $validator) 49 | { 50 | $validator 51 | ->add('id', 'valid', ['rule' => 'numeric']) 52 | ->allowEmpty('id', 'create') 53 | ->add('lft', 'valid', ['rule' => 'numeric']) 54 | ->allowEmpty('lft') 55 | ->add('rght', 'valid', ['rule' => 'numeric']) 56 | ->allowEmpty('rght') 57 | ->requirePresence('name', 'create') 58 | ->notEmpty('name') 59 | ->allowEmpty('description'); 60 | 61 | return $validator; 62 | } 63 | 64 | /** 65 | * Returns a rules checker object that will be used for validating 66 | * application integrity. 67 | * 68 | * @param \Cake\ORM\RulesChecker $rules The rules object to be modified. 69 | * @return \Cake\ORM\RulesChecker 70 | */ 71 | public function buildRules(RulesChecker $rules) 72 | { 73 | $rules->add($rules->existsIn(['parent_id'], 'ParentCategories')); 74 | return $rules; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /config/paths.php: -------------------------------------------------------------------------------- 1 | adaptor = new CakeFabricateAdaptor([ 47 | CakeFabricateAdaptor::OPTION_FILTER_KEY => true, 48 | CakeFabricateAdaptor::OPTION_VALIDATE => false 49 | ]); 50 | }); 51 | 52 | $this->fixtureInjector = new FixtureInjector(new FixtureManager()); 53 | $this->fixture = new BddAllFixture(); 54 | } 55 | 56 | /** @BeforeScenario */ 57 | public function beforeScenario(BeforeScenarioScope $scope) 58 | { 59 | $this->fixtureInjector->startTest($this->fixture); 60 | } 61 | 62 | /** @AfterScenario */ 63 | public function afterScenario(AfterScenarioScope $scope) 64 | { 65 | $this->fixtureInjector->endTest($this->fixture, time()); 66 | } 67 | 68 | 69 | } 70 | 71 | class BddAllFixture extends TestCase { 72 | public $fixtures = [ 73 | 'Categories' => 'app.categories', 74 | 'Articles' => 'app.articles', 75 | 'Users' => 'app.users', 76 | 'Categories' => 'app.categories' 77 | ]; 78 | } 79 | -------------------------------------------------------------------------------- /config/routes.php: -------------------------------------------------------------------------------- 1 | connect('/', ['controller' => 'Articles', 'action' => 'index']); 47 | 48 | /** 49 | * Connect catchall routes for all controllers. 50 | * 51 | * Using the argument `InflectedRoute`, the `fallbacks` method is a shortcut for 52 | * `$routes->connect('/:controller', ['action' => 'index'], ['routeClass' => 'InflectedRoute']);` 53 | * `$routes->connect('/:controller/:action/*', [], ['routeClass' => 'InflectedRoute']);` 54 | * 55 | * Any route class can be used with this method, such as: 56 | * - DashedRoute 57 | * - InflectedRoute 58 | * - Route 59 | * - Or your own route class 60 | * 61 | * You can remove these routes once you've connected the 62 | * routes you want in your application. 63 | */ 64 | $routes->fallbacks('InflectedRoute'); 65 | }); 66 | 67 | /** 68 | * Load all plugin routes. See the Plugin documentation on 69 | * how to customize the loading of plugin routes. 70 | */ 71 | Plugin::routes(); 72 | -------------------------------------------------------------------------------- /src/Shell/ConsoleShell.php: -------------------------------------------------------------------------------- 1 | err('Unable to load Psy\Shell.'); 37 | $this->err(''); 38 | $this->err('Make sure you have installed psysh as a dependency,'); 39 | $this->err('and that Psy\Shell is registered in your autoloader.'); 40 | $this->err(''); 41 | $this->err('If you are using composer run'); 42 | $this->err(''); 43 | $this->err('$ php composer.phar require --dev psy/psysh'); 44 | $this->err(''); 45 | return 1; 46 | } 47 | 48 | $this->out("You can exit with `CTRL-C` or `exit`"); 49 | $this->out(''); 50 | 51 | Log::drop('debug'); 52 | Log::drop('error'); 53 | $this->_io->setLoggers(false); 54 | restore_error_handler(); 55 | restore_exception_handler(); 56 | 57 | $psy = new PsyShell(); 58 | $psy->run(); 59 | } 60 | 61 | /** 62 | * Display help for this console. 63 | * 64 | * @return ConsoleOptionParser 65 | */ 66 | public function getOptionParser() 67 | { 68 | $parser = new ConsoleOptionParser('console', false); 69 | $parser->description( 70 | 'This shell provides a REPL that you can use to interact ' . 71 | 'with your application in an interactive fashion. You can use ' . 72 | 'it to run adhoc queries with your models, or experiment ' . 73 | 'and explore the features of CakePHP and your application.' . 74 | "\n\n" . 75 | 'You will need to have psysh installed for this Shell to work.' 76 | ); 77 | return $parser; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Template/Categories/index.ctp: -------------------------------------------------------------------------------- 1 |
2 |

3 |
    4 |
  • Html->link(__('New Category'), ['action' => 'add']) ?>
  • 5 |
  • Html->link(__('List Articles'), ['controller' => 'Articles', 'action' => 'index']) ?>
  • 6 |
  • Html->link(__('New Article'), ['controller' => 'Articles', 'action' => 'add']) ?>
  • 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 | 40 | 41 | 42 | 43 |
Paginator->sort('id') ?>Paginator->sort('parent_id') ?>Paginator->sort('lft') ?>Paginator->sort('rght') ?>Paginator->sort('name') ?>Paginator->sort('description') ?>Paginator->sort('created') ?>
Number->format($category->id) ?>Number->format($category->parent_id) ?>Number->format($category->lft) ?>Number->format($category->rght) ?>name) ?>description) ?>created) ?> 34 | Html->link(__('View'), ['action' => 'view', $category->id]) ?> 35 | Html->link(__('Edit'), ['action' => 'edit', $category->id]) ?> 36 | Form->postLink(__('Delete'), ['action' => 'delete', $category->id], ['confirm' => __('Are you sure you want to delete # {0}?', $category->id)]) ?> 37 | Form->postLink(__('Move down'), ['action' => 'move_down', $category->id], ['confirm' => __('Are you sure you want to move down # {0}?', $category->id)]) ?> 38 | Form->postLink(__('Move up'), ['action' => 'move_up', $category->id], ['confirm' => __('Are you sure you want to move up # {0}?', $category->id)]) ?> 39 |
44 |
45 |
    46 | Paginator->prev('< ' . __('previous')) ?> 47 | Paginator->numbers() ?> 48 | Paginator->next(__('next') . ' >') ?> 49 |
50 |

Paginator->counter() ?>

51 |
52 |
53 | -------------------------------------------------------------------------------- /src/Controller/ArticlesController.php: -------------------------------------------------------------------------------- 1 | loadComponent('Flash'); // Include the FlashComponent 13 | } 14 | 15 | public function index() 16 | { 17 | $this->set('articles', $this->Articles->find('all')); 18 | } 19 | 20 | public function view($id) 21 | { 22 | $article = $this->Articles->get($id); 23 | $this->set(compact('article')); 24 | } 25 | 26 | public function add() 27 | { 28 | $article = $this->Articles->newEntity(); 29 | if ($this->request->is('post')) { 30 | $article = $this->Articles->patchEntity($article, $this->request->data); 31 | // Added this line 32 | $article->user_id = $this->Auth->user('id'); 33 | if ($this->Articles->save($article)) { 34 | $this->Flash->success(__('Your article has been saved.')); 35 | return $this->redirect(['action' => 'index']); 36 | } 37 | $this->Flash->error(__('Unable to add your article.')); 38 | } 39 | $this->set('article', $article); 40 | 41 | // Just added the categories list to be able to choose 42 | // one category for an article 43 | $categories = $this->Articles->Categories->find('treeList'); 44 | $this->set(compact('categories')); 45 | } 46 | 47 | public function edit($id = null) 48 | { 49 | $article = $this->Articles->get($id); 50 | if ($this->request->is(['post', 'put'])) { 51 | $this->Articles->patchEntity($article, $this->request->data); 52 | if ($this->Articles->save($article)) { 53 | $this->Flash->success(__('Your article has been updated.')); 54 | return $this->redirect(['action' => 'index']); 55 | } 56 | $this->Flash->error(__('Unable to update your article.')); 57 | } 58 | 59 | $this->set('article', $article); 60 | } 61 | 62 | public function delete($id) 63 | { 64 | $this->request->allowMethod(['post', 'delete']); 65 | 66 | $article = $this->Articles->get($id); 67 | if ($this->Articles->delete($article)) { 68 | $this->Flash->success(__('The article with id: {0} has been deleted.', h($id))); 69 | return $this->redirect(['action' => 'index']); 70 | } 71 | } 72 | 73 | public function isAuthorized($user) 74 | { 75 | // All registered users can add articles 76 | if ($this->request->action === 'add') { 77 | return true; 78 | } 79 | 80 | // The owner of an article can edit and delete it 81 | if (in_array($this->request->action, ['edit', 'delete'])) { 82 | $articleId = (int)$this->request->params['pass'][0]; 83 | if ($this->Articles->isOwnedBy($articleId, $user['id'])) { 84 | return true; 85 | } 86 | } 87 | 88 | return parent::isAuthorized($user); 89 | } 90 | } -------------------------------------------------------------------------------- /src/Template/Categories/view.ctp: -------------------------------------------------------------------------------- 1 |
2 |

3 |
    4 |
  • Html->link(__('Edit Category'), ['action' => 'edit', $category->id]) ?>
  • 5 |
  • Form->postLink(__('Delete Category'), ['action' => 'delete', $category->id], ['confirm' => __('Are you sure you want to delete # {0}?', $category->id)]) ?>
  • 6 |
  • Html->link(__('List Categories'), ['action' => 'index']) ?>
  • 7 |
  • Html->link(__('New Category'), ['action' => 'add']) ?>
  • 8 |
  • Html->link(__('List Articles'), ['controller' => 'Articles', 'action' => 'index']) ?>
  • 9 |
  • Html->link(__('New Article'), ['controller' => 'Articles', 'action' => 'add']) ?>
  • 10 |
11 |
12 |
13 |

name) ?>

14 |
15 |
16 |
17 |

name) ?>

18 |
19 |

description) ?>

20 |
21 |
22 |
23 |

Number->format($category->id) ?>

24 |
25 |

Number->format($category->parent_id) ?>

26 |
27 |

Number->format($category->lft) ?>

28 |
29 |

Number->format($category->rght) ?>

30 |
31 |
32 |
33 |

created) ?>

34 |
35 |

modified) ?>

36 |
37 |
38 |
39 | 77 | -------------------------------------------------------------------------------- /src/Controller/CategoriesController.php: -------------------------------------------------------------------------------- 1 | paginate = [ 22 | 'contain' => ['ParentCategories'] 23 | ]; 24 | $this->set('categories', $this->paginate($this->Categories)); 25 | $this->set('_serialize', ['categories']); 26 | } 27 | 28 | public function move_up($id = null) 29 | { 30 | $this->request->allowMethod(['post', 'put']); 31 | $category = $this->Categories->get($id); 32 | if ($this->Categories->moveUp($category)) { 33 | $this->Flash->success('The category has been moved Up.'); 34 | } else { 35 | $this->Flash->error('The category could not be moved up. Please, try again.'); 36 | } 37 | return $this->redirect($this->referer(['action' => 'index'])); 38 | } 39 | 40 | public function move_down($id = null) 41 | { 42 | $this->request->allowMethod(['post', 'put']); 43 | $category = $this->Categories->get($id); 44 | if ($this->Categories->moveDown($category)) { 45 | $this->Flash->success('The category has been moved down.'); 46 | } else { 47 | $this->Flash->error('The category could not be moved down. Please, try again.'); 48 | } 49 | return $this->redirect($this->referer(['action' => 'index'])); 50 | } 51 | 52 | /** 53 | * View method 54 | * 55 | * @param string|null $id Category id. 56 | * @return void 57 | * @throws \Cake\Network\Exception\NotFoundException When record not found. 58 | */ 59 | public function view($id = null) 60 | { 61 | $category = $this->Categories->get($id, [ 62 | 'contain' => ['ParentCategories', 'Articles', 'ChildCategories'] 63 | ]); 64 | $this->set('category', $category); 65 | $this->set('_serialize', ['category']); 66 | } 67 | 68 | /** 69 | * Add method 70 | * 71 | * @return void Redirects on successful add, renders view otherwise. 72 | */ 73 | public function add() 74 | { 75 | $category = $this->Categories->newEntity(); 76 | if ($this->request->is('post')) { 77 | $category = $this->Categories->patchEntity($category, $this->request->data); 78 | if ($this->Categories->save($category)) { 79 | $this->Flash->success('The category has been saved.'); 80 | return $this->redirect(['action' => 'index']); 81 | } else { 82 | $this->Flash->error('The category could not be saved. Please, try again.'); 83 | } 84 | } 85 | $parentCategories = $this->Categories->ParentCategories->find('list', ['limit' => 200]); 86 | $this->set(compact('category', 'parentCategories')); 87 | $this->set('_serialize', ['category']); 88 | } 89 | 90 | /** 91 | * Edit method 92 | * 93 | * @param string|null $id Category id. 94 | * @return void Redirects on successful edit, renders view otherwise. 95 | * @throws \Cake\Network\Exception\NotFoundException When record not found. 96 | */ 97 | public function edit($id = null) 98 | { 99 | $category = $this->Categories->get($id, [ 100 | 'contain' => [] 101 | ]); 102 | if ($this->request->is(['patch', 'post', 'put'])) { 103 | $category = $this->Categories->patchEntity($category, $this->request->data); 104 | if ($this->Categories->save($category)) { 105 | $this->Flash->success('The category has been saved.'); 106 | return $this->redirect(['action' => 'index']); 107 | } else { 108 | $this->Flash->error('The category could not be saved. Please, try again.'); 109 | } 110 | } 111 | $parentCategories = $this->Categories->ParentCategories->find('list', ['limit' => 200]); 112 | $this->set(compact('category', 'parentCategories')); 113 | $this->set('_serialize', ['category']); 114 | } 115 | 116 | /** 117 | * Delete method 118 | * 119 | * @param string|null $id Category id. 120 | * @return void Redirects to index. 121 | * @throws \Cake\Network\Exception\NotFoundException When record not found. 122 | */ 123 | public function delete($id = null) 124 | { 125 | $this->request->allowMethod(['post', 'delete']); 126 | $category = $this->Categories->get($id); 127 | if ($this->Categories->delete($category)) { 128 | $this->Flash->success('The category has been deleted.'); 129 | } else { 130 | $this->Flash->error('The category could not be deleted. Please, try again.'); 131 | } 132 | return $this->redirect(['action' => 'index']); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /config/bootstrap.php: -------------------------------------------------------------------------------- 1 | getMessage() . "\n"); 69 | } 70 | 71 | // Load an environment local configuration file. 72 | // You can use a file like app_local.php to provide local overrides to your 73 | // shared configuration. 74 | //Configure::load('app_local', 'default'); 75 | 76 | // When debug = false the metadata cache should last 77 | // for a very very long time, as we don't want 78 | // to refresh the cache while users are doing requests. 79 | if (!Configure::read('debug')) { 80 | Configure::write('Cache._cake_model_.duration', '+1 years'); 81 | Configure::write('Cache._cake_core_.duration', '+1 years'); 82 | } 83 | 84 | /** 85 | * Set server timezone to UTC. You can change it to another timezone of your 86 | * choice but using UTC makes time calculations / conversions easier. 87 | */ 88 | date_default_timezone_set('UTC'); 89 | 90 | /** 91 | * Configure the mbstring extension to use the correct encoding. 92 | */ 93 | mb_internal_encoding(Configure::read('App.encoding')); 94 | 95 | /** 96 | * Set the default locale. This controls how dates, number and currency is 97 | * formatted and sets the default language to use for translations. 98 | */ 99 | ini_set('intl.default_locale', 'en_US'); 100 | 101 | /** 102 | * Register application error and exception handlers. 103 | */ 104 | $isCli = php_sapi_name() === 'cli'; 105 | if ($isCli) { 106 | (new ConsoleErrorHandler(Configure::read('Error')))->register(); 107 | } else { 108 | (new ErrorHandler(Configure::read('Error')))->register(); 109 | } 110 | 111 | // Include the CLI bootstrap overrides. 112 | if ($isCli) { 113 | require __DIR__ . '/bootstrap_cli.php'; 114 | } 115 | 116 | /** 117 | * Set the full base URL. 118 | * This URL is used as the base of all absolute links. 119 | * 120 | * If you define fullBaseUrl in your config file you can remove this. 121 | */ 122 | if (!Configure::read('App.fullBaseUrl')) { 123 | $s = null; 124 | if (env('HTTPS')) { 125 | $s = 's'; 126 | } 127 | 128 | $httpHost = env('HTTP_HOST'); 129 | if (isset($httpHost)) { 130 | Configure::write('App.fullBaseUrl', 'http' . $s . '://' . $httpHost); 131 | } 132 | unset($httpHost, $s); 133 | } 134 | 135 | Cache::config(Configure::consume('Cache')); 136 | ConnectionManager::config(Configure::consume('Datasources')); 137 | Email::configTransport(Configure::consume('EmailTransport')); 138 | Email::config(Configure::consume('Email')); 139 | Log::config(Configure::consume('Log')); 140 | Security::salt(Configure::consume('Security.salt')); 141 | 142 | /** 143 | * The default crypto extension in 3.0 is OpenSSL. 144 | * If you are migrating from 2.x uncomment this code to 145 | * use a more compatible Mcrypt based implementation 146 | */ 147 | // Security::engine(new \Cake\Utility\Crypto\Mcrypt()); 148 | 149 | /** 150 | * Setup detectors for mobile and tablet. 151 | */ 152 | Request::addDetector('mobile', function ($request) { 153 | $detector = new \Detection\MobileDetect(); 154 | return $detector->isMobile(); 155 | }); 156 | Request::addDetector('tablet', function ($request) { 157 | $detector = new \Detection\MobileDetect(); 158 | return $detector->isTablet(); 159 | }); 160 | 161 | /** 162 | * Custom Inflector rules, can be set to correctly pluralize or singularize 163 | * table, model, controller names or whatever other string is passed to the 164 | * inflection functions. 165 | * 166 | * Inflector::rules('plural', ['/^(inflect)or$/i' => '\1ables']); 167 | * Inflector::rules('irregular' => ['red' => 'redlings']); 168 | * Inflector::rules('uninflected', ['dontinflectme']); 169 | * Inflector::rules('transliteration', ['/å/' => 'aa']); 170 | */ 171 | 172 | /** 173 | * Plugins need to be loaded manually, you can either load them one by one or all of them in a single call 174 | * Uncomment one of the lines below, as you need. make sure you read the documentation on Plugin to use more 175 | * advanced ways of loading plugins 176 | * 177 | * Plugin::loadAll(); // Loads all plugins at once 178 | * Plugin::load('Migrations'); //Loads a single plugin named Migrations 179 | * 180 | */ 181 | 182 | Plugin::load('Migrations'); 183 | 184 | // Only try to load DebugKit in development mode 185 | // Debug Kit should not be installed on a production system 186 | if (Configure::read('debug')) { 187 | Plugin::load('DebugKit', ['bootstrap' => true]); 188 | } 189 | 190 | /** 191 | * Connect middleware/dispatcher filters. 192 | */ 193 | DispatcherFactory::add('Asset'); 194 | DispatcherFactory::add('Routing'); 195 | DispatcherFactory::add('ControllerFactory'); 196 | 197 | if (getenv('CAKE_ENV') === 'test') { 198 | ConnectionManager::alias('test', 'default'); 199 | } 200 | -------------------------------------------------------------------------------- /src/Console/Installer.php: -------------------------------------------------------------------------------- 1 | getIO(); 37 | 38 | $rootDir = dirname(dirname(__DIR__)); 39 | 40 | static::createAppConfig($rootDir, $io); 41 | static::createWritableDirectories($rootDir, $io); 42 | 43 | // ask if the permissions should be changed 44 | if ($io->isInteractive()) { 45 | $validator = function ($arg) { 46 | if (in_array($arg, ['Y', 'y', 'N', 'n'])) { 47 | return $arg; 48 | } 49 | throw new Exception('This is not a valid answer. Please choose Y or n.'); 50 | }; 51 | $setFolderPermissions = $io->askAndValidate( 52 | 'Set Folder Permissions ? (Default to Y) [Y,n]? ', 53 | $validator, 54 | 10, 55 | 'Y' 56 | ); 57 | 58 | if (in_array($setFolderPermissions, ['Y', 'y'])) { 59 | static::setFolderPermissions($rootDir, $io); 60 | } 61 | } else { 62 | static::setFolderPermissions($rootDir, $io); 63 | } 64 | 65 | static::setSecuritySalt($rootDir, $io); 66 | 67 | if (class_exists('\Cake\Codeception\Console\Installer')) { 68 | \Cake\Codeception\Console\Installer::customizeCodeceptionBinary($event); 69 | } 70 | } 71 | 72 | /** 73 | * Create the config/app.php file if it does not exist. 74 | * 75 | * @param string $dir The application's root directory. 76 | * @param \Composer\IO\IOInterface $io IO interface to write to console. 77 | * @return void 78 | */ 79 | public static function createAppConfig($dir, $io) 80 | { 81 | $appConfig = $dir . '/config/app.php'; 82 | $defaultConfig = $dir . '/config/app.default.php'; 83 | if (!file_exists($appConfig)) { 84 | copy($defaultConfig, $appConfig); 85 | $io->write('Created `config/app.php` file'); 86 | } 87 | } 88 | 89 | /** 90 | * Create the `logs` and `tmp` directories. 91 | * 92 | * @param string $dir The application's root directory. 93 | * @param \Composer\IO\IOInterface $io IO interface to write to console. 94 | * @return void 95 | */ 96 | public static function createWritableDirectories($dir, $io) 97 | { 98 | $paths = [ 99 | 'logs', 100 | 'tmp', 101 | 'tmp/cache', 102 | 'tmp/cache/models', 103 | 'tmp/cache/persistent', 104 | 'tmp/cache/views', 105 | 'tmp/sessions', 106 | 'tmp/tests' 107 | ]; 108 | 109 | foreach ($paths as $path) { 110 | $path = $dir . '/' . $path; 111 | if (!file_exists($path)) { 112 | mkdir($path); 113 | $io->write('Created `' . $path . '` directory'); 114 | } 115 | } 116 | } 117 | 118 | /** 119 | * Set globally writable permissions on the "tmp" and "logs" directory. 120 | * 121 | * This is not the most secure default, but it gets people up and running quickly. 122 | * 123 | * @param string $dir The application's root directory. 124 | * @param \Composer\IO\IOInterface $io IO interface to write to console. 125 | * @return void 126 | */ 127 | public static function setFolderPermissions($dir, $io) 128 | { 129 | // Change the permissions on a path and output the results. 130 | $changePerms = function ($path, $perms, $io) { 131 | // Get current permissions in decimal format so we can bitmask it. 132 | $currentPerms = octdec(substr(sprintf('%o', fileperms($path)), -4)); 133 | if (($currentPerms & $perms) == $perms) { 134 | return; 135 | } 136 | 137 | $res = chmod($path, $currentPerms | $perms); 138 | if ($res) { 139 | $io->write('Permissions set on ' . $path); 140 | } else { 141 | $io->write('Failed to set permissions on ' . $path); 142 | } 143 | }; 144 | 145 | $walker = function ($dir, $perms, $io) use (&$walker, $changePerms) { 146 | $files = array_diff(scandir($dir), ['.', '..']); 147 | foreach ($files as $file) { 148 | $path = $dir . '/' . $file; 149 | 150 | if (!is_dir($path)) { 151 | continue; 152 | } 153 | 154 | $changePerms($path, $perms, $io); 155 | $walker($path, $perms, $io); 156 | } 157 | }; 158 | 159 | $worldWritable = bindec('0000000111'); 160 | $walker($dir . '/tmp', $worldWritable, $io); 161 | $changePerms($dir . '/tmp', $worldWritable, $io); 162 | $changePerms($dir . '/logs', $worldWritable, $io); 163 | } 164 | 165 | /** 166 | * Set the security.salt value in the application's config file. 167 | * 168 | * @param string $dir The application's root directory. 169 | * @param \Composer\IO\IOInterface $io IO interface to write to console. 170 | * @return void 171 | */ 172 | public static function setSecuritySalt($dir, $io) 173 | { 174 | $config = $dir . '/config/app.php'; 175 | $content = file_get_contents($config); 176 | 177 | $newKey = hash('sha256', $dir . php_uname() . microtime(true)); 178 | $content = str_replace('__SALT__', $newKey, $content, $count); 179 | 180 | if ($count == 0) { 181 | $io->write('No Security.salt placeholder to replace.'); 182 | return; 183 | } 184 | 185 | $result = file_put_contents($config, $content); 186 | if ($result) { 187 | $io->write('Updated Security.salt value in config/app.php'); 188 | return; 189 | } 190 | $io->write('Unable to update Security.salt value.'); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /webroot/css/cake.css: -------------------------------------------------------------------------------- 1 | a.disabled { 2 | pointer-events: none; 3 | } 4 | 5 | a:hover { 6 | color: #15848F; 7 | } 8 | 9 | a { 10 | color: #1798A5; 11 | } 12 | 13 | .side-nav li a:not(.button) { 14 | color: #15848F; 15 | } 16 | 17 | .side-nav li a:not(.button):hover { 18 | color: #15848F; 19 | } 20 | 21 | header { 22 | background-color: #15848F; 23 | color: #ffffff; 24 | font-size: 30px; 25 | height: 84px; 26 | line-height: 64px; 27 | padding: 16px 0px; 28 | box-shadow: 0px 1px rgba(0, 0, 0, 0.24); 29 | } 30 | 31 | header .header-title { 32 | padding-left:80px 33 | } 34 | 35 | 36 | legend { 37 | color:#15848F; 38 | } 39 | 40 | .row { 41 | max-width: 80rem; 42 | } 43 | 44 | .actions.columns { 45 | margin-top:1rem; 46 | border-left: 5px solid #15848F; 47 | padding-left: 15px; 48 | padding: 32px 20px; 49 | } 50 | 51 | .actions.columns h3 { 52 | color:#15848F; 53 | } 54 | 55 | .index table { 56 | margin-top: 2rem; 57 | border: 0; 58 | } 59 | 60 | .index table thead { 61 | height: 3.5rem; 62 | } 63 | 64 | .header-help { 65 | float: right; 66 | margin-right:2rem; 67 | margin-top: -80px; 68 | font-size:16px; 69 | } 70 | 71 | .header-help span { 72 | font-weight: normal; 73 | text-align: center; 74 | text-decoration: none; 75 | line-height: 1; 76 | white-space: nowrap; 77 | display: inline-block; 78 | padding: 0.25rem 0.5rem 0.375rem; 79 | font-size: 0.8rem; 80 | background-color: #0097a7; 81 | color: #FFF; 82 | border-radius: 1000px; 83 | } 84 | 85 | .header-help a { 86 | color: #fff; 87 | } 88 | 89 | ul.pagination li a { 90 | color: rgba(0, 0 ,0 , 0.54); 91 | } 92 | 93 | ul.pagination li.active a { 94 | background: none repeat scroll 0% 0% #DCE47E; 95 | color: #FFF; 96 | font-weight: bold; 97 | cursor: default; 98 | } 99 | 100 | .paginator { 101 | text-align: center; 102 | } 103 | 104 | .paginator ul.pagination li { 105 | float: none; 106 | display: inline-block; 107 | } 108 | 109 | .paginator p { 110 | text-align: right; 111 | color: rgba(0, 0 ,0 , 0.54); 112 | } 113 | 114 | button { 115 | background: #8D6E65; 116 | } 117 | 118 | .form button:hover, .form button:focus { 119 | background: #7A6058; 120 | box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.26) !important; 121 | } 122 | 123 | .form button[type="submit"] { 124 | float: right; 125 | text-transform: uppercase; 126 | box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.26); 127 | } 128 | 129 | .form .error-message { 130 | display: block; 131 | padding: 0.375rem 0.5625rem 0.5625rem; 132 | margin-top: -1px; 133 | margin-bottom: 1rem; 134 | font-size: 0.75rem; 135 | font-weight: normal; 136 | font-style: italic; 137 | color: rgba(0, 0, 0, 0.54); 138 | } 139 | 140 | .required > label { 141 | font-weight: bold; 142 | } 143 | .required > label:after { 144 | content: ' *'; 145 | color: #C3232D; 146 | } 147 | 148 | select[multiple] { 149 | min-height:150px; 150 | background: none; 151 | } 152 | input[type=checkbox], 153 | input[type=radio] { 154 | margin-right: 0.5em; 155 | } 156 | 157 | .date select, 158 | .time select, 159 | .datetime select { 160 | display: inline; 161 | width: auto; 162 | margin-right: 10px; 163 | } 164 | 165 | .error label, 166 | .error label.error { 167 | color: #C3232D; 168 | } 169 | 170 | div.message { 171 | border-style: solid; 172 | border-width: 1px; 173 | display: block; 174 | font-weight: normal; 175 | position: relative; 176 | padding: 0.875rem 1.5rem 0.875rem 20%; 177 | transition: opacity 300ms ease-out 0s; 178 | background-color: #DCE47E; 179 | border-color: #DCE47E; 180 | color: #626262; 181 | } 182 | 183 | div.message.error { 184 | background-color: #C3232D; 185 | border-color: #C3232D; 186 | color: #FFF; 187 | } 188 | 189 | div.message:before { 190 | line-height: 0px; 191 | font-size: 20px; 192 | height: 12px; 193 | width: 12px; 194 | border-radius: 15px; 195 | text-align: center; 196 | vertical-align: middle; 197 | display: inline-block; 198 | position: relative; 199 | left: -11px; 200 | background-color: #FFF; 201 | padding: 12px 14px 12px 10px; 202 | content: "i"; 203 | color: #DCE47E; 204 | } 205 | 206 | div.message.error:before { 207 | padding: 11px 16px 14px 7px; 208 | color: #C3232D; 209 | content: "x"; 210 | } 211 | 212 | .view h2 { 213 | color: #6F6F6F; 214 | } 215 | 216 | .view .columns.strings { 217 | border-radius: 3px; 218 | box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.24); 219 | margin-right:0.7rem; 220 | } 221 | 222 | .view .numbers { 223 | background-color: #B7E3EC; 224 | color: #FFF; 225 | border-radius: 3px; 226 | box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.24); 227 | margin-right: 0.7rem; 228 | } 229 | 230 | .view .columns.dates { 231 | border-radius: 3px; 232 | box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.24); 233 | margin-right:0.7rem; 234 | background-color:#DCE47E; 235 | color: #fff; 236 | } 237 | 238 | .view .columns.booleans { 239 | border-radius: 3px; 240 | box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.24); 241 | margin-right:0.7rem; 242 | background-color: #8D6E65; 243 | color: #fff; 244 | } 245 | 246 | .view .strings p { 247 | border-bottom: 1px solid #eee; 248 | } 249 | .view .numbers .subheader, .view .dates .subheader { 250 | color:#747474; 251 | } 252 | .view .booleans .subheader { 253 | color: #E9E9E9 254 | } 255 | 256 | .view .texts .columns { 257 | margin-top:1.2rem; 258 | border-bottom: 1px solid #eee; 259 | } 260 | 261 | 262 | /** Notices and Errors **/ 263 | .cake-error, 264 | .cake-debug, 265 | .notice, 266 | p.error, 267 | p.notice { 268 | display: block; 269 | clear: both; 270 | background-repeat: repeat-x; 271 | margin-bottom: 18px; 272 | padding: 7px 14px; 273 | border-radius: 3px; 274 | box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.24); 275 | } 276 | 277 | .cake-debug, 278 | .notice, 279 | p.notice { 280 | color: #000000; 281 | background: #ffcc00; 282 | } 283 | 284 | .cake-error, 285 | p.error { 286 | color: #fff; 287 | background: #C3232D; 288 | } 289 | 290 | pre { 291 | background: none repeat scroll 0% 0% #FFF; 292 | box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.24); 293 | margin: 15px 0px; 294 | color: rgba(0, 0 ,0 , 0.74); 295 | padding:5px; 296 | } 297 | 298 | .cake-error .cake-stack-trace { 299 | margin-top:10px; 300 | } 301 | 302 | .cake-stack-trace code { 303 | background: inherit; 304 | border:0; 305 | } 306 | 307 | .cake-code-dump .code-highlight { 308 | display: block; 309 | background-color: #FFC600; 310 | } 311 | 312 | .cake-error a, 313 | .cake-error a:hover { 314 | color:#fff; 315 | text-decoration: underline; 316 | } 317 | 318 | .home header { 319 | width: 100%; 320 | height: 85%; 321 | position: relative; 322 | display: table; 323 | } 324 | 325 | .home h1 { 326 | font-family: "Gill Sans MT", Calibri, sans-serif; 327 | } 328 | 329 | .home header .header-image { 330 | display: table-cell; 331 | vertical-align: middle; 332 | text-align: center; 333 | } 334 | 335 | .home header h1 { 336 | color: #fff; 337 | } 338 | 339 | .home .checks { 340 | padding:30px; 341 | color: #626262; 342 | border-radius: 3px; 343 | box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.24); 344 | margin-top:50px; 345 | } 346 | 347 | .checks.platform { 348 | background-color: #B7E3EC; 349 | } 350 | 351 | .checks.filesystem { 352 | background: #DCE47E; 353 | } 354 | 355 | .checks.database { 356 | background-color: #DFF0D8; 357 | padding-bottom: 10px; 358 | margin-bottom: 30px; 359 | } 360 | 361 | .home .checks .success:before, .home .checks .problem:before { 362 | line-height: 0px; 363 | font-size: 28px; 364 | height: 12px; 365 | width: 12px; 366 | border-radius: 15px; 367 | text-align: center; 368 | vertical-align: middle; 369 | display: inline-block; 370 | position: relative; 371 | left: -11px; 372 | } 373 | 374 | .home .checks .success:before { 375 | content: "✓"; 376 | color: green; 377 | margin-right: 9px; 378 | } 379 | 380 | .home .checks .problem:before { 381 | content: "✘"; 382 | color: red; 383 | margin-right: 9px; 384 | } 385 | -------------------------------------------------------------------------------- /src/Template/Pages/home.ctp: -------------------------------------------------------------------------------- 1 | layout = false; 22 | 23 | if (!Configure::read('debug')): 24 | throw new NotFoundException(); 25 | endif; 26 | 27 | $cakeDescription = 'CakePHP: the rapid development php framework'; 28 | ?> 29 | 30 | 31 | 32 | Html->charset() ?> 33 | 34 | 35 | <?= $cakeDescription ?> 36 | 37 | Html->meta('icon') ?> 38 | Html->css('base.css') ?> 39 | Html->css('cake.css') ?> 40 | 41 | 42 |
43 |
44 | Html->image('http://cakephp.org/img/cake-logo.png') ?> 45 |

Get the Ovens Ready

46 |
47 |
48 |
49 | 54 | 59 | 60 |
61 |
62 | =')): ?> 63 |

Your version of PHP is 5.4.16 or higher.

64 | 65 |

Your version of PHP is too low. You need PHP 5.4.16 or higher to use CakePHP.

66 | 67 | 68 | 69 |

Your version of PHP has the mbstring extension loaded.

70 | 71 |

Your version of PHP does NOT have the mbstring extension loaded.

; 72 | 73 | 74 | 75 |

Your version of PHP has the openssl extension loaded.

76 | 77 |

Your version of PHP has the mcrypt extension loaded.

78 | 79 |

Your version of PHP does NOT have the openssl or mcrypt extension loaded.

80 | 81 | 82 | 83 |

Your version of PHP has the intl extension loaded.

84 | 85 |

Your version of PHP does NOT have the intl extension loaded.

86 | 87 |
88 |
89 | 90 |

Your tmp directory is writable.

91 | 92 |

Your tmp directory is NOT writable.

93 | 94 | 95 | 96 |

Your logs directory is writable.

97 | 98 |

Your logs directory is NOT writable.

99 | 100 | 101 | 102 | 103 |

The Engine is being used for core caching. To change the config edit config/app.php

104 | 105 |

Your cache is NOT working. Please check the settings in config/app.php

106 | 107 |
108 |
109 |
110 |
111 | connect(); 115 | } catch (Exception $connectionError) { 116 | $connected = false; 117 | $errorMsg = $connectionError->getMessage(); 118 | if (method_exists($connectionError, 'getAttributes')): 119 | $attributes = $connectionError->getAttributes(); 120 | if (isset($errorMsg['message'])): 121 | $errorMsg .= '
' . $attributes['message']; 122 | endif; 123 | endif; 124 | } 125 | ?> 126 | 127 |

CakePHP is able to connect to the database.

128 | 129 |

CakePHP is NOT able to connect to the database.

130 | 131 |
132 |
133 |
134 |
135 |

Editing this Page

136 |
    137 |
  • To change the content of this page, edit: src/Template/Pages/home.ctp.
  • 138 |
  • You can also add some CSS styles for your pages at: webroot/css/.
  • 139 |
140 |
141 |
142 |

Getting Started

143 | 148 |

149 |

150 |
151 | 152 |
153 |
154 |
155 |

More about Cake

156 |

157 | CakePHP is a rapid development framework for PHP which uses commonly known design patterns like Front Controller and MVC. 158 |

159 |

160 | Our primary goal is to provide a structured framework that enables PHP users at all levels to rapidly develop robust web applications, without any loss to flexibility. 161 |

162 | 163 | 185 |
186 |
187 |
188 |
189 |
190 | 191 | 192 | -------------------------------------------------------------------------------- /config/app.default.php: -------------------------------------------------------------------------------- 1 | true, 13 | 14 | /** 15 | * Configure basic information about the application. 16 | * 17 | * - namespace - The namespace to find app classes under. 18 | * - encoding - The encoding used for HTML + database connections. 19 | * - base - The base directory the app resides in. If false this 20 | * will be auto detected. 21 | * - dir - Name of app directory. 22 | * - webroot - The webroot directory. 23 | * - wwwRoot - The file path to webroot. 24 | * - baseUrl - To configure CakePHP to *not* use mod_rewrite and to 25 | * use CakePHP pretty URLs, remove these .htaccess 26 | * files: 27 | * /.htaccess 28 | * /webroot/.htaccess 29 | * And uncomment the baseUrl key below. 30 | * - fullBaseUrl - A base URL to use for absolute links. 31 | * - imageBaseUrl - Web path to the public images directory under webroot. 32 | * - cssBaseUrl - Web path to the public css directory under webroot. 33 | * - jsBaseUrl - Web path to the public js directory under webroot. 34 | * - paths - Configure paths for non class based resources. Supports the 35 | * `plugins`, `templates`, `locales` subkeys, which allow the definition of 36 | * paths for plugins, view templates and locale files respectively. 37 | */ 38 | 'App' => [ 39 | 'namespace' => 'App', 40 | 'encoding' => 'UTF-8', 41 | 'base' => false, 42 | 'dir' => 'src', 43 | 'webroot' => 'webroot', 44 | 'wwwRoot' => WWW_ROOT, 45 | // 'baseUrl' => env('SCRIPT_NAME'), 46 | 'fullBaseUrl' => false, 47 | 'imageBaseUrl' => 'img/', 48 | 'cssBaseUrl' => 'css/', 49 | 'jsBaseUrl' => 'js/', 50 | 'paths' => [ 51 | 'plugins' => [ROOT . DS . 'plugins' . DS], 52 | 'templates' => [APP . 'Template' . DS], 53 | 'locales' => [APP . 'Locale' . DS], 54 | ], 55 | ], 56 | 57 | /** 58 | * Security and encryption configuration 59 | * 60 | * - salt - A random string used in security hashing methods. 61 | * The salt value is also used as the encryption key. 62 | * You should treat it as extremely sensitive data. 63 | */ 64 | 'Security' => [ 65 | 'salt' => '__SALT__', 66 | ], 67 | 68 | /** 69 | * Apply timestamps with the last modified time to static assets (js, css, images). 70 | * Will append a querystring parameter containing the time the file was modified. 71 | * This is useful for busting browser caches. 72 | * 73 | * Set to true to apply timestamps when debug is true. Set to 'force' to always 74 | * enable timestamping regardless of debug value. 75 | */ 76 | 'Asset' => [ 77 | // 'timestamp' => true, 78 | ], 79 | 80 | /** 81 | * Configure the cache adapters. 82 | */ 83 | 'Cache' => [ 84 | 'default' => [ 85 | 'className' => 'File', 86 | 'path' => CACHE, 87 | ], 88 | 89 | /** 90 | * Configure the cache used for general framework caching. Path information, 91 | * object listings, and translation cache files are stored with this 92 | * configuration. 93 | */ 94 | '_cake_core_' => [ 95 | 'className' => 'File', 96 | 'prefix' => 'myapp_cake_core_', 97 | 'path' => CACHE . 'persistent/', 98 | 'serialize' => true, 99 | 'duration' => '+2 minutes', 100 | ], 101 | 102 | /** 103 | * Configure the cache for model and datasource caches. This cache 104 | * configuration is used to store schema descriptions, and table listings 105 | * in connections. 106 | */ 107 | '_cake_model_' => [ 108 | 'className' => 'File', 109 | 'prefix' => 'myapp_cake_model_', 110 | 'path' => CACHE . 'models/', 111 | 'serialize' => true, 112 | 'duration' => '+2 minutes', 113 | ], 114 | ], 115 | 116 | /** 117 | * Configure the Error and Exception handlers used by your application. 118 | * 119 | * By default errors are displayed using Debugger, when debug is true and logged 120 | * by Cake\Log\Log when debug is false. 121 | * 122 | * In CLI environments exceptions will be printed to stderr with a backtrace. 123 | * In web environments an HTML page will be displayed for the exception. 124 | * With debug true, framework errors like Missing Controller will be displayed. 125 | * When debug is false, framework errors will be coerced into generic HTTP errors. 126 | * 127 | * Options: 128 | * 129 | * - `errorLevel` - int - The level of errors you are interested in capturing. 130 | * - `trace` - boolean - Whether or not backtraces should be included in 131 | * logged errors/exceptions. 132 | * - `log` - boolean - Whether or not you want exceptions logged. 133 | * - `exceptionRenderer` - string - The class responsible for rendering 134 | * uncaught exceptions. If you choose a custom class you should place 135 | * the file for that class in src/Error. This class needs to implement a 136 | * render method. 137 | * - `skipLog` - array - List of exceptions to skip for logging. Exceptions that 138 | * extend one of the listed exceptions will also be skipped for logging. 139 | * E.g.: 140 | * `'skipLog' => ['Cake\Network\Exception\NotFoundException', 'Cake\Network\Exception\UnauthorizedException']` 141 | */ 142 | 'Error' => [ 143 | 'errorLevel' => E_ALL & ~E_DEPRECATED, 144 | 'exceptionRenderer' => 'Cake\Error\ExceptionRenderer', 145 | 'skipLog' => [], 146 | 'log' => true, 147 | 'trace' => true, 148 | ], 149 | 150 | /** 151 | * Email configuration. 152 | * 153 | * You can configure email transports and email delivery profiles here. 154 | * 155 | * By defining transports separately from delivery profiles you can easily 156 | * re-use transport configuration across multiple profiles. 157 | * 158 | * You can specify multiple configurations for production, development and 159 | * testing. 160 | * 161 | * ### Configuring transports 162 | * 163 | * Each transport needs a `className`. Valid options are as follows: 164 | * 165 | * Mail - Send using PHP mail function 166 | * Smtp - Send using SMTP 167 | * Debug - Do not send the email, just return the result 168 | * 169 | * You can add custom transports (or override existing transports) by adding the 170 | * appropriate file to src/Network/Email. Transports should be named 171 | * 'YourTransport.php', where 'Your' is the name of the transport. 172 | * 173 | * ### Configuring delivery profiles 174 | * 175 | * Delivery profiles allow you to predefine various properties about email 176 | * messages from your application and give the settings a name. This saves 177 | * duplication across your application and makes maintenance and development 178 | * easier. Each profile accepts a number of keys. See `Cake\Network\Email\Email` 179 | * for more information. 180 | */ 181 | 'EmailTransport' => [ 182 | 'default' => [ 183 | 'className' => 'Mail', 184 | // The following keys are used in SMTP transports 185 | 'host' => 'localhost', 186 | 'port' => 25, 187 | 'timeout' => 30, 188 | 'username' => 'user', 189 | 'password' => 'secret', 190 | 'client' => null, 191 | 'tls' => null, 192 | ], 193 | ], 194 | 195 | 'Email' => [ 196 | 'default' => [ 197 | 'transport' => 'default', 198 | 'from' => 'you@localhost', 199 | //'charset' => 'utf-8', 200 | //'headerCharset' => 'utf-8', 201 | ], 202 | ], 203 | 204 | /** 205 | * Connection information used by the ORM to connect 206 | * to your application's datastores. 207 | * Drivers include Mysql Postgres Sqlite Sqlserver 208 | * See vendor\cakephp\cakephp\src\Database\Driver for complete list 209 | */ 210 | 'Datasources' => [ 211 | 'default' => [ 212 | 'className' => 'Cake\Database\Connection', 213 | 'driver' => 'Cake\Database\Driver\Mysql', 214 | 'persistent' => false, 215 | 'host' => 'localhost', 216 | /** 217 | * CakePHP will use the default DB port based on the driver selected 218 | * MySQL on MAMP uses port 8889, MAMP users will want to uncomment 219 | * the following line and set the port accordingly 220 | */ 221 | //'port' => 'nonstandard_port_number', 222 | 'username' => 'my_app', 223 | 'password' => 'secret', 224 | 'database' => 'my_app', 225 | 'encoding' => 'utf8', 226 | 'timezone' => 'UTC', 227 | 'cacheMetadata' => true, 228 | 229 | /** 230 | * Set identifier quoting to true if you are using reserved words or 231 | * special characters in your table or column names. Enabling this 232 | * setting will result in queries built using the Query Builder having 233 | * identifiers quoted when creating SQL. It should be noted that this 234 | * decreases performance because each query needs to be traversed and 235 | * manipulated before being executed. 236 | */ 237 | 'quoteIdentifiers' => false, 238 | 239 | /** 240 | * During development, if using MySQL < 5.6, uncommenting the 241 | * following line could boost the speed at which schema metadata is 242 | * fetched from the database. It can also be set directly with the 243 | * mysql configuration directive 'innodb_stats_on_metadata = 0' 244 | * which is the recommended value in production environments 245 | */ 246 | //'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'], 247 | ], 248 | 249 | /** 250 | * The test connection is used during the test suite. 251 | */ 252 | 'test' => [ 253 | 'className' => 'Cake\Database\Connection', 254 | 'driver' => 'Cake\Database\Driver\Mysql', 255 | 'persistent' => false, 256 | 'host' => 'localhost', 257 | //'port' => 'nonstandard_port_number', 258 | 'username' => 'my_app', 259 | 'password' => 'secret', 260 | 'database' => 'test_myapp', 261 | 'encoding' => 'utf8', 262 | 'timezone' => 'UTC', 263 | 'cacheMetadata' => true, 264 | 'quoteIdentifiers' => false, 265 | //'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'], 266 | ], 267 | ], 268 | 269 | /** 270 | * Configures logging options 271 | */ 272 | 'Log' => [ 273 | 'debug' => [ 274 | 'className' => 'Cake\Log\Engine\FileLog', 275 | 'path' => LOGS, 276 | 'file' => 'debug', 277 | 'levels' => ['notice', 'info', 'debug'], 278 | ], 279 | 'error' => [ 280 | 'className' => 'Cake\Log\Engine\FileLog', 281 | 'path' => LOGS, 282 | 'file' => 'error', 283 | 'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'], 284 | ], 285 | ], 286 | 287 | /** 288 | * 289 | * Session configuration. 290 | * 291 | * Contains an array of settings to use for session configuration. The 292 | * `defaults` key is used to define a default preset to use for sessions, any 293 | * settings declared here will override the settings of the default config. 294 | * 295 | * ## Options 296 | * 297 | * - `cookie` - The name of the cookie to use. Defaults to 'CAKEPHP'. 298 | * - `cookiePath` - The url path for which session cookie is set. Maps to the 299 | * `session.cookie_path` php.ini config. Defaults to base path of app. 300 | * - `timeout` - The time in minutes the session should be valid for. 301 | * Pass 0 to disable checking timeout. 302 | * - `defaults` - The default configuration set to use as a basis for your session. 303 | * There are four built-in options: php, cake, cache, database. 304 | * - `handler` - Can be used to enable a custom session handler. Expects an 305 | * array with at least the `engine` key, being the name of the Session engine 306 | * class to use for managing the session. CakePHP bundles the `CacheSession` 307 | * and `DatabaseSession` engines. 308 | * - `ini` - An associative array of additional ini values to set. 309 | * 310 | * The built-in `defaults` options are: 311 | * 312 | * - 'php' - Uses settings defined in your php.ini. 313 | * - 'cake' - Saves session files in CakePHP's /tmp directory. 314 | * - 'database' - Uses CakePHP's database sessions. 315 | * - 'cache' - Use the Cache class to save sessions. 316 | * 317 | * To define a custom session handler, save it at src/Network/Session/.php. 318 | * Make sure the class implements PHP's `SessionHandlerInterface` and set 319 | * Session.handler to 320 | * 321 | * To use database sessions, load the SQL file located at config/Schema/sessions.sql 322 | */ 323 | 'Session' => [ 324 | 'defaults' => 'php', 325 | ], 326 | ]; 327 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CakePHP3 + Behat and PHPSpec 2 | 3 | This is CakePHP3 Blog Tutorial Application Example For Testing by Behat and PHPSpec 4 | 5 | ## Installing and Runnning the example 6 | 7 | ### Install 8 | 9 | I use Cakebox for creating application. 10 | Using it, easy to run the example application. 11 | 12 | 1: Install Vagrant and Virtualbox 13 | 14 | - [Vagrant](https://www.vagrantup.com/) 15 | - [Virtualbox](https://www.virtualbox.org/) 16 | 17 | 2: Install Cakebox 18 | 19 | ``` 20 | localhost:any $ git clone https://github.com/alt3/cakebox.git 21 | localhost:any $ cd cakebox 22 | localhost:cakebox $ cp Cakebox.yaml.default Cakebox.yaml 23 | localhost:cakebox $ vagrant up 24 | ``` 25 | 26 | upgrade your box to Ubuntu 16.04 with PHP7.1 by running: 27 | 28 | ``` 29 | localhost:cakebox $ vagrant ssh 30 | vagrant@cakebox $ sudo apt-get update 31 | vagrant@cakebox $ sudo apt-get install software-properties-common python-software-properties 32 | vagrant@cakebox $ /cakebox/bash/ubuntu-16.sh 33 | vagrant@cakebox $ exit 34 | localhost:cakebox $ vagrant reload 35 | ``` 36 | 37 | Check PHP version: 38 | 39 | ``` 40 | localhost:cakebox $ vagrant ssh 41 | vagrant@cakebox:~$ php -v 42 | PHP 7.1.12-2+ubuntu16.04.1+deb.sury.org+2 (cli) (built: Dec 7 2017 20:12:04) ( NTS ) 43 | ``` 44 | 45 | 3: Install example application 46 | 47 | ``` 48 | localhost:cakebox $ vagrant ssh 49 | Welcome to Ubuntu 16.04.3 LTS (GNU/Linux 4.4.0-103-generic x86_64) 50 | 51 | vagrant@cakebox $ cakebox application add blog-tutorial.app --source https://github.com/sizuhiko/cakephp3-bdd-example.git --webroot /home/vagrant/Apps/blog-tutorial.app/webroot 52 | ``` 53 | 54 | It will print out logs of installation followings: 55 | 56 | ``` 57 | Creating application http://blog-tutorial.app 58 | 59 | Configuring installer 60 | Creating installation directory 61 | Git installing user specified application sources 62 | Creating virtual host 63 | * Successfully created PHP-FPM virtual host 64 | Creating databases 65 | * Successfully created main database 66 | * Successfully created test database 67 | Configuring permissions 68 | Updating configuration files 69 | Application created using: 70 | database => blog-tutorial_app 71 | framework_human => user specified 72 | framework_short => custom 73 | installation_method => git 74 | path => /home/vagrant/Apps/blog-tutorial.app 75 | source => https://github.com/sizuhiko/cakephp3-bdd-example.git 76 | url => blog-tutorial.app 77 | webroot => /home/vagrant/Apps/blog-tutorial.app/webroot 78 | Please note: 79 | => Configuration files are not automatically updated for user specified applications. 80 | => Make sure to manually update your database credentials, plugins, etc. 81 | 82 | Remember to update your hosts file with: 10.33.10.10 http://blog-tutorial.app 83 | 84 | Installation completed successfully 85 | ``` 86 | 87 | After installation completed successfully, create directories and install dependencies. 88 | 89 | ``` 90 | vagrant@cakebox $ cd Apps/blog-tutorial.app 91 | vagrant@cakebox:~/Apps/blog-tutorial.app$ mkdir tmp 92 | vagrant@cakebox:~/Apps/blog-tutorial.app$ mkdir logs 93 | vagrant@cakebox:~/Apps/blog-tutorial.app$ cp config/app.default.php config/app.php 94 | vagrant@cakebox:~/Apps/blog-tutorial.app$ composer install 95 | 96 | Generating autoload files 97 | Set Folder Permissions ? (Default to Y) [Y,n]? 98 | Updated Security.salt value in config/app.php 99 | ``` 100 | 101 | ### Configuration 102 | 103 | #### Example Application Config 104 | 105 | Edit section of database connection in `config/app.php`. 106 | 107 | ```php 108 | 'Datasources' => [ 109 | 'default' => [ 110 | 'className' => 'Cake\Database\Connection', 111 | 'driver' => 'Cake\Database\Driver\Mysql', 112 | 'persistent' => false, 113 | 'host' => 'localhost', 114 | 'username' => 'cakebox', // CHANGE 115 | 'password' => 'secret', 116 | 'database' => 'blog-tutorial_app', // CHANGE 117 | 'encoding' => 'utf8', 118 | 'timezone' => 'UTC', 119 | 'cacheMetadata' => true, 120 | 'quoteIdentifiers' => false, 121 | ], 122 | 'test' => [ 123 | 'className' => 'Cake\Database\Connection', 124 | 'driver' => 'Cake\Database\Driver\Mysql', 125 | 'persistent' => false, 126 | 'host' => 'localhost', 127 | 'username' => 'cakebox', // CHANGE 128 | 'password' => 'secret', 129 | 'database' => 'test_blog-tutorial_app', // CHANGE 130 | 'encoding' => 'utf8', 131 | 'timezone' => 'UTC', 132 | 'cacheMetadata' => true, 133 | 'quoteIdentifiers' => false, 134 | ], 135 | ``` 136 | 137 | #### Host Computer Config 138 | 139 | Add host name to your host computer 140 | For example followings: 141 | 142 | ``` 143 | localhost:cakebox $ sudo vi /etc/hosts 144 | ``` 145 | 146 | Add `10.33.10.10 blog-tutorial.app`. 147 | 148 | #### Cakebox Config 149 | 150 | For running behat, recommend to change some configurations. 151 | 152 | 1: Up box memory to 2048M. 153 | 154 | ```yaml 155 | # Cakebox.yml 156 | vm: 157 | hostname: cakebox 158 | ip: 10.33.10.10 159 | memory: 2048 160 | cpus: 1 161 | ``` 162 | 163 | 2: Set xdebug.max_nesting_level 164 | 165 | ``` 166 | localhost:cakebox $ sudo vi /etc/php/7.1/fpm/conf.d/20-xdebug.ini 167 | ``` 168 | 169 | ```ini 170 | # /etc/php/7.1/fpm/conf.d/20-xdebug.ini 171 | xdebug.max_nesting_level=500 172 | ``` 173 | 174 | #### Webserver Config On Cakebox 175 | 176 | I provided configuration of nginx for testing. 177 | If you use Cakebox, then copy the config file like followings: 178 | 179 | ``` 180 | vagrant@cakebox $ cd ~/Apps/blog-tutorial.app 181 | vagrant@cakebox:~/Apps/blog-tutorial.app$ sudo cp blog-tutorial.app.test /etc/nginx/sites-available/ 182 | vagrant@cakebox:~/Apps/blog-tutorial.app$ sudo ln -s /etc/nginx/sites-available/blog-tutorial.app.test /etc/nginx/sites-enabled/ 183 | vagrant@cakebox:~/Apps/blog-tutorial.app$ sudo service nginx restart 184 | ``` 185 | 186 | And add `/etc/hosts` on Cakebox vm to hostname for testing. 187 | 188 | ``` 189 | localhost:cakebox $ sudo vi /etc/hosts 190 | 191 | # Add host 192 | 10.33.10.10 blog-tutorial.app.test 193 | ``` 194 | 195 | ### Migrate Database 196 | 197 | Create all tables for application by migration command. 198 | 199 | ``` 200 | bin/cake migrations migrate 201 | ``` 202 | 203 | ### Run Test 204 | 205 | After installations and configurations completed successfully, run test using Behat. 206 | 207 | ``` 208 | vagrant@cakebox:~/Apps/blog-tutorial.app$ vendor/bin/behat 209 | ``` 210 | 211 | It will print out test results followings: 212 | 213 | ``` 214 | Feature: 215 | In order to tell the masses what's on my mind 216 | As a user 217 | I want to read articles on the site 218 | 219 | Background: # features/articles.feature:7 220 | Given there is a post: # ArticlesContext::thereIsAPost() 221 | | Title | Body | 222 | | The title | This is the post body. | 223 | | A title once again | And the post body follows. | 224 | | Title strikes back | This is really exciting! Not. | 225 | And there is a user: # UsersContext::thereIsAUser() 226 | | Username | Password | FirstName | LastName | 227 | | alice | ecila | Alice | Smith | 228 | | bob | obo | Bob | Johnson | 229 | And there is a category: # CategoriesContext::thereIsACategory() 230 | | Name | 231 | | Events | 232 | | Computers | 233 | | Foods | 234 | 235 | Scenario: Show articles # features/articles.feature:23 236 | When I am on "TopPage" # WebContext::visit() 237 | Then I should see "The title" # WebContext::assertPageContainsText() 238 | And I should see "A title once again" # WebContext::assertPageContainsText() 239 | And I should see "Title strikes back" # WebContext::assertPageContainsText() 240 | 241 | Scenario: Show the article # features/articles.feature:29 242 | Given I am on "TopPage" # WebContext::visit() 243 | When I follow "A title once again" # WebContext::clickLink() 244 | Then I should see "And the post body follows." # WebContext::assertPageContainsText() 245 | 246 | Scenario: Add new article # features/articles.feature:34 247 | Given I am on "TopPage" # WebContext::visit() 248 | And I follow "Add" # WebContext::clickLink() 249 | And I login "bob" "obo" # UsersContext::iLogin() 250 | When I post article form : # ArticlesContext::iPostArticleForm() 251 | | Label | Value | 252 | | Categories | Events | 253 | | Title | Today is Party | 254 | | Body | From 19:30 with Alice | 255 | And I should see "Your article has been saved." # WebContext::assertPageContainsText() 256 | And I should see "Today is party" # WebContext::assertPageContainsText() 257 | 258 | Scenario: Remove article # features/articles.feature:46 259 | Given I am on "TopPage" # WebContext::visit() 260 | When I delete article "Title strikes back" # ArticlesContext::iDeleteArticle() 261 | Then I should not see "Title strikes back" # WebContext::assertPageNotContainsText() 262 | 263 | 4 scenarios (4 passed) 264 | 28 steps (28 passed) 265 | 0m10.65s (41.29Mb) 266 | ``` 267 | 268 | 269 | ## How to made this example 270 | 271 | This section explains about steps of creation for the example application. 272 | 273 | ### Create CakePHP3 Blog Tutorial Application 274 | 275 | I use Cakebox for creating application. 276 | 277 | 1. Install Vagrant and Virtualbox. 278 | 2. Install Cakebox 279 | 3. Generate CakePHP3 application skelton by cakebox 280 | 281 | ``` 282 | localhost:cakebox $ vagrant ssh 283 | Welcome to Ubuntu 14.04.1 LTS (GNU/Linux 3.13.0-24-generic x86_64) 284 | 285 | vagrant@cakebox:~$ cakebox application add blog-tutorial.app 286 | ``` 287 | 288 | After generate application completed successfully, add host name to your host computer 289 | 290 | ``` 291 | localhost:cakebox $ sudo vi /etc/hosts 292 | ``` 293 | 294 | Add `10.33.10.10 blog-tutorial.app`. 295 | 296 | Continue to read tutorial docs, bake and write codes. 297 | 298 | [Blog tutorial](http://book.cakephp.org/3.0/en/tutorials-and-examples/blog/blog.html) 299 | 300 | Any configuration was finished by Cakebox. 301 | 302 | ### Install Behat/Mink and any components by composer 303 | 304 | For testing, use following components: 305 | 306 | - Mink Extension (dependent Mink and Behat) 307 | - Mink goutte driver (Headless web driver) 308 | - PHPUnit (for using CakePHP fixture manager) 309 | - PHP dotenv (for switching environment) 310 | - Cake Fabricate (Testdata generator) 311 | 312 | ``` 313 | vagrant@cakebox:~$ cd Apps/blog-tutorial.app/ 314 | vagrant@cakebox:~/Apps/blog-tutorial.app$ composer require --dev behat/mink-extension 315 | vagrant@cakebox:~/Apps/blog-tutorial.app$ composer require --dev behat/mink-goutte-driver 316 | vagrant@cakebox:~/Apps/blog-tutorial.app$ composer require --dev phpunit/phpunit 317 | vagrant@cakebox:~/Apps/blog-tutorial.app$ composer require --dev sizuhiko/cake_fabricate 318 | vagrant@cakebox:~/Apps/blog-tutorial.app$ composer require vlucas/phpdotenv 319 | ``` 320 | 321 | #### Generate behat skelton 322 | 323 | Behat has feature for generation skelton. 324 | Run `behat --init` on application root directory. 325 | 326 | ``` 327 | vagrant@cakebox:~$ cd Apps/blog-tutorial.app/ 328 | vagrant@cakebox:~/Apps/blog-tutorial.app$ vendor/bin/behat --init 329 | ``` 330 | 331 | #### Add behat.yml 332 | 333 | Create `behat.yml` on `Apps/blog-tutorial.app` directory. 334 | And activate `Mink Extension`. 335 | See [Mink Extension Official documentation](https://github.com/Behat/MinkExtension/blob/master/doc/index.rst). 336 | 337 | ```yaml 338 | # behat.yml 339 | default: 340 | # ... 341 | extensions: 342 | Behat\MinkExtension: 343 | base_url: 'http://blog-tutorial.app.test/' 344 | sessions: 345 | default: 346 | goutte: ~ 347 | suites: 348 | my_suite: 349 | contexts: 350 | - FeatureContext 351 | - Behat\MinkExtension\Context\MinkContext 352 | ``` 353 | 354 | #### Add posts.feature 355 | 356 | Add `posts.feature` onto `features` directory. 357 | See example code. 358 | 359 | ### For switching environment 360 | 361 | For testing by behat, you should switch application environment. 362 | Because use test database when accessed by behat. 363 | On this example application, uses CAKE_ENV environment. 364 | Add `config/bootstrap.php` for switching database env. 365 | 366 | ```php 367 | if (getenv('CAKE_ENV') === 'test') { 368 | ConnectionManager::alias('test', 'default'); 369 | } 370 | ``` 371 | 372 | I provided configuration of nginx for testing. 373 | If you use Cakebox, then copy the config file like followings: 374 | 375 | ```bash 376 | sudo cp blog-tutorial.app.test /etc/nginx/sites-available/ 377 | sudo ln -s /etc/nginx/sites-available/blog-tutorial.app.test /etc/nginx/sites-enabled/ 378 | sudo service nginx restart 379 | ``` 380 | 381 | And add `/etc/hosts` on Cakebox vm to hostname for testing. 382 | 383 | ```bash 384 | localhost:cakebox $ sudo vi /etc/hosts 385 | 386 | # Add host 387 | 10.33.10.10 blog-tutorial.app.test 388 | ``` 389 | 390 | ### Integrate CakePHP3 and Behat 391 | 392 | `features/bootstrap/FeatureContext.php` is bootstrap of Behat test. 393 | 394 | ```php 395 | class FeatureContext implements Context, SnippetAcceptingContext 396 | { 397 | public function __construct() 398 | { 399 | require_once dirname(dirname(__DIR__)) . '/tests/bootstrap.php'; // (1) 400 | 401 | // Always connect test database 402 | ConnectionManager::alias('test', 'default'); // (2) 403 | 404 | Fabricate::config(function($config) { // (3) 405 | $config->adaptor = new CakeFabricateAdaptor([ 406 | CakeFabricateAdaptor::OPTION_FILTER_KEY => true, 407 | CakeFabricateAdaptor::OPTION_VALIDATE => false 408 | ]); 409 | }); 410 | 411 | $this->fixtureInjector = new FixtureInjector(new FixtureManager()); //(4) 412 | $this->fixture = new BddAllFixture(); 413 | } 414 | } 415 | ``` 416 | 417 | This bootstrap flow (especially 1 and 4) inspired from `phpunit.xml.dist` of CakePHP3. 418 | (1) is from `phpunit` tag. Load `bootstrap.php` for testing CakePHP application. 419 | (4) is from `listeners` tag. For using fixture system into behat step. 420 | 421 | ```xml 422 | 423 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | ./tests/TestCase 439 | 440 | 441 | 442 | 443 | 444 | 445 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | ``` 455 | 456 | (2) is connection of (default) database for testing. 457 | (3) is configuration for integration fabricate to CakePHP. 458 | 459 | 460 | ### Using fixture of CakePHP on context of Behat 461 | 462 | In the example case, Fixtures define on FeatureContext as previously described. 463 | 464 | ```php 465 | $this->fixtureInjector = new FixtureInjector(new FixtureManager()); //(4) 466 | $this->fixture = new BddAllFixture(); 467 | ``` 468 | 469 | Behat provides some hook points. 470 | Fixtures are loaded and unloaded with this. 471 | 472 | ```php 473 | /** @BeforeScenario */ 474 | public function beforeScenario(BeforeScenarioScope $scope) 475 | { 476 | $this->fixtureInjector->startTest($this->fixture); 477 | } 478 | 479 | /** @AfterScenario */ 480 | public function afterScenario(AfterScenarioScope $scope) 481 | { 482 | $this->fixtureInjector->endTest($this->fixture, time()); 483 | } 484 | ``` 485 | 486 | `@BeforeScenario` hook is run before a specific scenario will run. 487 | `@AfterScenario` hook is run after Behat finishes executing a scenario. 488 | 489 | Inside CakePHP, `FixtureInjector` takes a role as PHPUnit_Framework_TestListener. 490 | 491 | ```php 492 | class FixtureInjector implements PHPUnit_Framework_TestListener 493 | { 494 | 495 | /** 496 | * Adds fixtures to a test case when it starts. 497 | * 498 | * @param \PHPUnit_Framework_Test $test The test case 499 | * @return void 500 | */ 501 | public function startTest(PHPUnit_Framework_Test $test) 502 | { 503 | $test->fixtureManager = $this->_fixtureManager; 504 | if ($test instanceof TestCase) { 505 | $this->_fixtureManager->fixturize($test); 506 | $this->_fixtureManager->load($test); 507 | } 508 | } 509 | 510 | /** 511 | * Unloads fixtures from the test case. 512 | * 513 | * @param \PHPUnit_Framework_Test $test The test case 514 | * @param float $time current time 515 | * @return void 516 | */ 517 | public function endTest(PHPUnit_Framework_Test $test, $time) 518 | { 519 | if ($test instanceof TestCase) { 520 | $this->_fixtureManager->unload($test); 521 | } 522 | } 523 | 524 | } 525 | ``` 526 | 527 | Without PHPUnit, it should call these hook functions. 528 | FeatureContext hooks simulate the listener functions. 529 | 530 | At last, FixtureInjector startTest and endTest functions are required of arguments as PHPUnit_Framework_Test. 531 | So, it should create class extends TestCase. 532 | In the example, I create BddAllFixture class into `FeatureContext.php`. 533 | It only has `$fixtures` array for FixtureInjector. 534 | 535 | ```php 536 | class BddAllFixture extends TestCase { 537 | public $fixtures = [ 538 | 'Categories' => 'app.categories', 539 | 'Articles' => 'app.articles', 540 | 'Users' => 'app.users', 541 | 'Categories' => 'app.categories' 542 | ]; 543 | } 544 | ``` 545 | 546 | ### Using any CakePHP3 feature in contexts of Behat 547 | 548 | You can use any CakePHP3 feature in contexts of Behat. 549 | In the example, it calls CakePHP Router::url() at `WebContext.php`. 550 | 551 | ```php 552 | use Behat\MinkExtension\Context\MinkContext; 553 | use Cake\Routing\Router; 554 | 555 | class WebContext extends MinkContext 556 | { 557 | public function locatePath($path) 558 | { 559 | return parent::locatePath($this->getPathTo($path)); 560 | } 561 | 562 | private function getPathTo($path) 563 | { 564 | switch ($path) { 565 | case 'TopPage': return Router::url(['controller' => 'articles', 'action' => 'index']); 566 | case 'トップページ': return Router::url(['controller' => 'articles', 'action' => 'index']); 567 | default: return $path; 568 | } 569 | } 570 | } 571 | ``` 572 | 573 | CakePHP3 feature can use in any context of Behat. 574 | Because [CakePHP3 is fully adopt PSR-2](http://bakery.cakephp.org/2014/12/16/CakePHP-3-to-fully-adopt-PSR-2.html), it is awesome and take it easy. 575 | 576 | ### Write Step to Contexts 577 | 578 | The example app has 5 context classes. 579 | 580 | - FeatureContext is bootstrap. It constructor integrates to CakePHP. 581 | - WebContext extends MinkContext to testing for web application. Overwrite locatePath for using alias of url. 582 | - ArticlesContext has steps about Articles model and the pages. 583 | - UsersContext has steps about Users model, the pages and authentication. 584 | - CategoriesContext has steps about Category model. 585 | 586 | The steps list to `behat.yml`. 587 | 588 | ```yaml 589 | suites: 590 | default: 591 | contexts: 592 | - FeatureContext 593 | - WebContext 594 | - ArticlesContext 595 | - UsersContext 596 | - CategoriesContext 597 | ``` 598 | 599 | 600 | ## TODO 601 | 602 | - PHPSpec integration documentation and example test code. 603 | 604 | --------------------------------------------------------------------------------