├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── config_example.php ├── lagan-logo.svg ├── models └── lagan │ ├── Crew.php │ ├── Feature.php │ └── Hoverkraft.php ├── public ├── .htaccess ├── files │ ├── crew-1.jpg │ ├── crew-2.jpg │ ├── crew-3.jpg │ ├── hoverkraft-1.jpg │ ├── hoverkraft-2.jpg │ └── hoverkraft-3.jpg └── index.php ├── routes ├── admin.php ├── functions.php ├── public.php └── static.php ├── setup.php ├── templates ├── admin │ ├── base.html │ ├── bean.html │ ├── beans.html │ └── index.html ├── public │ ├── base.html │ ├── hoverkraft.html │ ├── index.html │ └── search.html └── static │ ├── 404.html │ └── hello-world.html ├── tests ├── LaganTest.php ├── README.md └── functions.php └── twigextensions └── CsrfExtension.php /.gitignore: -------------------------------------------------------------------------------- 1 | # OS generated files # 2 | ###################### 3 | .DS_Store 4 | .DS_Store? 5 | ._* 6 | .Spotlight-V100 7 | .Trashes 8 | ehthumbs.db 9 | Thumbs.db 10 | 11 | # Config file # 12 | ############### 13 | config.php 14 | 15 | # Directory contents # 16 | ###################### 17 | vendor/* 18 | cache/* 19 | public/property-templates/* 20 | public/uploads/* 21 | 22 | # Composer # 23 | ############ 24 | composer.lock 25 | composer.phar -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 - 2018 Lútsen Stellingwerff 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Lagan 3 |

4 |

Any content, with a backend

5 |

Lagan lets you create flexible content objects with a simple class,
and manage them with a web interface that is 'automagically' created.

6 | 7 | Why Lagan? 8 | ---------- 9 | 10 | Lagan is a different take on a CMS, with a focus on flexibility. 11 | 12 | - Content models are easily created and modified 13 | - Content models consist of a simple combination of arrays 14 | - Content models can be any combination of properties 15 | - Configuration and editing are separated 16 | - All configuration is done by code, so developers are in control there 17 | - Content can be edited with a web interface, so editors can do their thing 18 | - Lagan is built on proven open-source PHP libraries 19 | - It is easy to extend with new content property types 20 | - Create Twig front-end templates to display your content the way you want 21 | 22 | Lagan is built with my favourite PHP libraries: 23 | - [Slim framework](http://www.slimframework.com/) 24 | - [RedBean ORM](http://redbeanphp.com/) 25 | - [Twig template engine](http://twig.sensiolabs.org/) 26 | 27 | 28 | 29 | Requirements 30 | ------------ 31 | 32 | - PHP 5.5 or newer 33 | - A database. I use MySQL in this repo for now, but others should work as well. [Check out the RedBean documentation for that](http://redbeanphp.com/index.php?p=/connection). 34 | - An Apache webserver if you want to use the .htaccess URL rewriting. But other webservers should work as well; [check out the Slim documentation for that](http://www.slimframework.com/docs/start/web-servers.html). 35 | - [PDO plus driver for your database](http://php.net/manual/en/book.pdo.php) (Usually installed) 36 | - [Multibyte String Support](http://php.net/manual/en/book.mbstring.php) (Usually installed too) 37 | 38 | 39 | 40 | Install Lagan 41 | ============= 42 | 43 | Install Lagan and its dependencies with [Composer](https://getcomposer.org/) with this command: `$ php composer.phar create-project lagan/lagan [project-name]` 44 | (Replace [project-name] with the desired directory name for your new project) 45 | 46 | The Composer script creates the *cache* directory, *config.php* file and RedBean *rb.php* file for you. 47 | 48 | Update *config.php* with: 49 | - your database settings 50 | - your server paths 51 | - the admin user(s) and their password(s) 52 | 53 | Lagan uses [Slim HTTP Basic Authentication middleware](http://www.appelsiini.net/projects/slim-basic-auth) to authenticate users for the admin interface. Make sure to change the password in *config.php*, and use HTTPS to login securely. 54 | 55 | 56 | 57 | Use Lagan 58 | ========= 59 | 60 | 61 | Content models 62 | -------------- 63 | 64 | After installing Lagan, you can begin adding your content models. This is where the "magic" of Lagan happens. Each type of content has it's own model. I added 3 example models, *Crew.php*, *Feature.php* and *Hoverkraft.php*. If you open them you will see they have a type, a description and an aray with different content properties. 65 | 66 | You can add your own content models by just adding class files like this to the *models/lagan* directory. Lagan will automatically create and update database tables for them. Nice! 67 | [> More about the content model structure](#structure-of-a-lagan-model) 68 | 69 | 70 | 71 | Web interface 72 | ------------- 73 | You can enter the Lagan web interface by going to the */admin* directory on the webserver where you installed Lagan. Here you can log in with the username and password you added in the *config.php* file. Now you can add or edit content objects based on the Lagan models. 74 | 75 | 76 | 77 | Routes 78 | ------ 79 | 80 | In the directory *routes* you can add your public routes to the *public.php* file. You can add your own route files as well. The routes are automatically included in your Lagan app. 81 | 82 | In the routes you can use the Lagan model CRUD methods to read and manipulate your data. 83 | [> More about the content model methods](#methods-of-a-lagan-model) 84 | 85 | 86 | 87 | Templates 88 | --------- 89 | 90 | Lagan uses [Twig](http://twig.sensiolabs.org/) as its template engine. You can add your templates to the *templates/public* directory and add them to your routes to use them in your app. 91 | 92 | 93 | 94 | Structure of a Lagan model 95 | -------------------------- 96 | 97 | All Lagan content models extend the *Lagan* main model. They contain a type, a description and an aray with different content properties. 98 | The *Lagan* main model is part of the [Lagan Core](https://packagist.org/packages/lagan/core) repository. 99 | 100 | A simple Lagan model looks like this: 101 | 102 | ```php 103 | namespace Lagan\Model; 104 | 105 | class Book extends \Lagan\Lagan { 106 | 107 | function __construct() { 108 | $this->type = 'book'; 109 | 110 | $this->description = 'These objects contain information about a book.'; 111 | 112 | $this->properties = [ 113 | [ 114 | 'name' => 'title', 115 | 'description' => 'The book title', 116 | 'type' => '\Lagan\Property\Str', 117 | 'input' => 'text' 118 | ] 119 | ]; 120 | } 121 | 122 | } 123 | ``` 124 | 125 | ### Type ### 126 | 127 | `$this->type` is the type of the model. It is the same as the modelname in lowercase, and defines the name of the RedBean beans and the name of the table in the database. 128 | 129 | 130 | ### Description ### 131 | 132 | `$this->description` is the description of the model. It is displayed in the admin interface. It explains the function of the content model to the user. 133 | 134 | 135 | ### Properties ### 136 | 137 | `$this->properties` are the properties of the model. They are an array defining the different content data-fields of the model. Each content model should always have a string type property named title. 138 | 139 | Each property is an array with the following keys: 140 | 141 | - *name*: Required. The name of the property. Also the name of the corresponding RedBean property. Contains only alphanumeric characters, should not contain spaces. 142 | - *description*: Required. The form-field label of the property in the admin interface. 143 | - *input*: Required. The template to use in the admin interface. Templates are located in the *public/property-templates* directory. 144 | - *type*: Required. The type of data of the property. This defines which property type controller to use. More information under ["Property types"](#property-type-controllers). 145 | - *required*: Optional. Set to true if the property is required. 146 | - *autovalue*: Optional. Set to true if the property needs to set it's own value. This forces a value for a property, also if it is not submitted on creation. Like a slug or a UID for example. 147 | - *searchable*: Optional. Set to true if the property has to be searchable with the Search controller. 148 | - *unique*: Optional. Set to true if the value of this property has to be unique for this model. 149 | 150 | There can be other optional keys for specific input types, for example the *directory* key for the *image_select* property input type. 151 | 152 | A properties array with more keys might look like this: 153 | 154 | ```php 155 | $this->properties = [ 156 | [ 157 | 'name' => 'title', 158 | 'description' => 'The book title', 159 | 'required' => true, 160 | 'searchable' => true, 161 | 'type' => '\Lagan\Property\Str', 162 | 'input' => 'text', 163 | 'validate' => 'minlength(3)' 164 | ] 165 | ]; 166 | ``` 167 | 168 | 169 | Methods of a Lagan model 170 | ------------------------ 171 | 172 | All Lagan content models extend the *Lagan* main model. Doing so they inherit it's methods. 173 | Lagan offers the CRUD methods: *Create*, *Read*, *Update* and *Delete*. 174 | 175 | Lagan uses [RedBean](http://redbeanphp.com/) to manipulate data in the database. Redbean returns data from the database as objects called [beans](http://redbeanphp.com/crud/). 176 | 177 | 178 | ### Create ### 179 | 180 | `create($data)` creates a RedBean bean in the database, based on the corresponding Lagan content model, and returns it. The *$data* variable is an array with at least the required properties. The array can be your HTML form POST data. 181 | 182 | ```php 183 | $book = new \Lagan\Model\Book; 184 | $bean = $book->create($data); 185 | ``` 186 | 187 | 188 | ### Read ### 189 | 190 | `read($id)` reads a bean based on the corresponding Lagan model from the database and returns it. The *$id* variable is the id of the Lagan model bean. 191 | 192 | Read a Book model bean with id 1: 193 | ```php 194 | $bean = $book->read(1); 195 | ``` 196 | 197 | Read all Book model beans: 198 | ```php 199 | $beans = $book->read(); 200 | ``` 201 | 202 | ### Update ### 203 | 204 | `update($data, $id)` updates a bean based on the corresponding Lagan model from the database and returns it. The *$data* variable is an array with at least the required properties. The array can be your HTML form POST data. The *$id* variable is the id of the Lagan model bean. 205 | 206 | Update the Book model bean with id 1: 207 | ```php 208 | $bean = $book->update($data, 1); 209 | ``` 210 | 211 | 212 | ### Delete ### 213 | 214 | `delete($id)` deletes a bean based on the corresponding Lagan model from the database. The *$id* variable is the id of the Lagan model bean. 215 | 216 | Delete the Book model bean with id 1: 217 | ```php 218 | $bean = $book->delete(1); 219 | ``` 220 | 221 | 222 | 223 | Searching entries of a Lagan model 224 | ---------------------------------- 225 | 226 | Each Lagan content model can be searched using the Search controller. The search controller is part of the [Lagan Core](https://packagist.org/packages/lagan/core) repository. 227 | Start by setting up the search controller in a route like this: `$search = new \Lagan\Search('book');` 228 | The search model now can use the GET request parameters to perform a search: `$search->find( $request->getParams() )` 229 | It can only search properties that are set to be [searchable](#properties). 230 | 231 | ```php 232 | $search = new \Lagan\Search('book'); 233 | $result = $search->find( $request->getParams() ); 234 | ``` 235 | 236 | Search has the following options: 237 | 238 | - From: *min 239 | - To: *max 240 | - Contains: *has 241 | - Equal to: *is 242 | - Sort: sort by property. `asc` sorts ascending and `desc` sorts descending 243 | - Limit: limit number of results 244 | - Offset: where to start returning results 245 | 246 | Offset only works if limit is defined too. 247 | 248 | Some query structure examples: 249 | `path/to/search?*has=[search string]`: Searches all searchable properties of a model 250 | `path/to/search?[property]*has=[search string]`: Searches single [property] of a model 251 | `path/to/search?[property]*min=[number]`: Searches all model with a minimum [number] value of [property] 252 | `path/to/search?[property]*has=[search string]&sort=[property]*asc`: Searches single [property] of a model and sorts the result ascending 253 | 254 | 255 | That's it! Now you know everything you need to know to start using Lagan. 256 | Want to extend Lagan? Read on! 257 | 258 | 259 | 260 | Extend Lagan 261 | ============ 262 | 263 | You can extend Lagan by adding your own property types to it. All Lagan property controllers are separate dependencies. You can include them to your Lagan app with Composer. To edit properties in the Lagan web interface you need a property template. You can add new property templates to Lagan with Composer too. Check out the *composer.json* file to see which properties and templates are included. 264 | 265 | 266 | 267 | Property type controllers 268 | ------------------------- 269 | 270 | Each property type controller is a dependency, added with Composer. This way new property types can be developed seperate from the Lagan project code. These are the property types now installed by Composer when installing Lagan: 271 | 272 | - **Boolean**: [\Lagan\Property\Boolean](https://packagist.org/packages/lagan/property-boolean) 273 | Lets the user set a boolean vanlue (true or false), for example with a checkbox 274 | 275 | - **File select**: [\Lagan\Property\Fileselect](https://packagist.org/packages/lagan/property-fileselect) 276 | Lets the user select a file from a directory 277 | 278 | - **Hashid**: [\Lagan\Property\Hashid](https://packagist.org/packages/lagan/property-hashid) 279 | Generates YouTube-like ids based on the conten object id's 280 | 281 | - **Instaembed**: [\Lagan\Property\Instaembed](https://packagist.org/packages/lagan/property-instaembed) 282 | Stores the Instagram embed code of the corresponding Instagram post id 283 | 284 | - **Many to many**: [\Lagan\Property\Manytomany](https://packagist.org/packages/lagan/property-manytomany) 285 | Define a many-to-many relation between two content entries 286 | 287 | - **Many to one**: [\Lagan\Property\Manytoone](https://packagist.org/packages/lagan/property-manytoone) 288 | Define a may-to-one relation between two content objects 289 | 290 | - **One to many**: [\Lagan\Property\Onetomany](https://packagist.org/packages/lagan/property-onetomany) 291 | Define a one-to-many relation between two content objects 292 | 293 | - **Objectlink**: [\Lagan\Property\Objectlink](https://packagist.org/packages/lagan/property-objectlink) 294 | Can link an object to any object of any type in the database 295 | 296 | - **Password hash**: [\Lagan\Property\Passwordhash](https://packagist.org/packages/lagan/property-passwordhash) 297 | Generate password hashes using the PHP password_hash function 298 | 299 | - **Position**: [\Lagan\Property\Position](https://packagist.org/packages/lagan/property-position) 300 | Define the order of content objects of the same type 301 | 302 | - **Slug**: [\Lagan\Property\Slug](https://packagist.org/packages/lagan/property-slug) 303 | Creates a slug from a string, and checks if it's unique 304 | 305 | - **String**: [\Lagan\Property\Str](https://packagist.org/packages/lagan/property-string) 306 | Input and validate a string 307 | 308 | - **Upload**: [\Lagan\Property\Upload](https://packagist.org/packages/lagan/property-upload) 309 | Lets the user upload a file 310 | 311 | 312 | ### Property type controller methods ### 313 | 314 | A property type controller can contain a *set*, *read*, *delete* and *options* method. All methods are optional. 315 | 316 | - The **set** method is executed each time a property with this type is set. 317 | - The **read** method is executed each time a property with this type is read. 318 | Note: For performance reasons, the read method is only executed for reading a single bean. Related beans are not returned. 319 | - The **delete** method is executed each time a an entry with a property with this type is deleted. 320 | - The **options** method returns all possible values for this property. 321 | 322 | 323 | Property input templates 324 | ------------------------ 325 | 326 | To edit a property in the backend web interface it needs a template. Each property template is also a dependency, added with Composer. They are put in the *public/property-templates* directory, so outside the vendor directory. This is done using a [Composer plugin](https://packagist.org/packages/lagan/template-installer-plugin). By placing them outside the Vendor directory they can contain stuff like Javascript or images. 327 | 328 | Currently these templates are available: 329 | 330 | - **[checkbox](https://packagist.org/packages/lagan/template-checkbox)** 331 | Template for a checkbox input, can be used with the boolean property. 332 | 333 | - **[fileselect](https://packagist.org/packages/lagan/template-fileselect)** 334 | Template to edit Lagan fileselect properties. 335 | 336 | - **[instaembed](https://packagist.org/packages/lagan/template-instaembed)** 337 | Template to edit Lagan instaembed property. 338 | 339 | - **[manytoone](https://packagist.org/packages/lagan/template-manytoone)** 340 | Template to edit Lagan many-to-one properties. 341 | 342 | - **[tomany](https://packagist.org/packages/lagan/template-tomany)** 343 | Template to edit Lagan one-to-many and many-to-many properties. 344 | 345 | - **[objectlink](https://packagist.org/packages/lagan/template-objectlink)** 346 | Template to edit Lagan objectlink properties. 347 | 348 | - **[readonly](https://packagist.org/packages/lagan/template-readonly)** 349 | Template for properties that can not be edited by the content editor. 350 | 351 | - **[text](https://packagist.org/packages/lagan/template-text)** 352 | Template for Lagan properties that require text input. 353 | 354 | - **[textarea](https://packagist.org/packages/lagan/template-textarea)** 355 | Textarea template for Lagan properties that require multiple lines of text input. 356 | 357 | - **[trumbowyg](https://packagist.org/packages/lagan/template-trumbowyg)** 358 | Template that turns a textarea into a WYSIWYG editor field with the [Trumbowyg WYSIWYG editor](https://alex-d.github.io/Trumbowyg/). 359 | 360 | - **[upload](https://packagist.org/packages/lagan/template-upload)** 361 | Template for Lagan upload properties. 362 | 363 | 364 | The properties of the property and the content bean are available in the template. To get the property name for example, use this Twig syntax: `{{ property.name }}`. To get the content of the specific property, use `{{ bean[property.name] }}`. 365 | 366 | 367 | JSON API 368 | -------- 369 | 370 | The [Lagan JSON API route repository](https://github.com/lutsen/Lagan-JSON-API-route) contains a route file to add a JSON API to your Lagan project. To install, add the *api.php* file to the *routes* directory. To protect the */api/write* route, add it to the Slim HTTP Basic Authentication middleware setup in the index.php file: 371 | `'path' => ['/admin', '/api/write']`. 372 | 373 | I also created a [Todo Backend](http://todobackend.com/) implementation with Lagan ([Run the specs](http://todobackend.com/specs/index.html?https://www.laganphp.com/todobackend/todo), [view the code](https://github.com/lutsen/lagan-todobackend)). 374 | 375 | 376 | Different admin template 377 | ------------------------ 378 | If you're tired of the default admin temlate, try this one: 379 | https://github.com/Skayo/Lagan-Bulma-Theme 380 | 381 | 382 | 383 | Lagan project structure 384 | ======================= 385 | 386 | An overview of the directories of a Lagan app and their contents. 387 | 388 | 389 | #### cache (directory) #### 390 | 391 | The Composer script creates this directory in the project root to hold the Twig template engine cache files. If updates in your templates are not showing; remember to clear the cache directory. 392 | 393 | 394 | #### models/lagan (directory) #### 395 | 396 | Contains all the different Lagan content models. They are in a seperate *lagan* directory so you can add your own models to the main *model* directory. 397 | 398 | 399 | #### public (directory) #### 400 | 401 | Contains the *index.php* and *.htaccess* file. The *index.php* file includes the setup.php and route files, and includes some other files and settings. 402 | 403 | *The "public" directory is the directory holding your public web pages on your webserver. It's name can vary on different hosting providers and -environments. Other common names are "html", "private-html", "www" or "web". Put the files of the "public" directory in this public directory on your webserver.* 404 | 405 | 406 | #### public/property-templates (directory) #### 407 | 408 | Created by [Composer](https://getcomposer.org/). Here Composer will add all the templates needed to edit a property in the backend web interface. 409 | 410 | 411 | #### routes (directory) #### 412 | 413 | Contains the different route files. Each route file is automatically loaded, and contains the routes for your project. Routes are built with [Slim](http://www.slimframework.com/). Data is retrieved using Lagan models, or by using [RedBean](http://redbeanphp.com/) directly. You can add your own route files here, or add them to an existing route file. 414 | This directory also contains *functions.php* which contains some route helper functions used in multiple route files. 415 | 416 | 417 | #### twigextensions (directory) ### 418 | 419 | Contains the [Twig extensions](https://twig.symfony.com/doc/2.x/advanced.html). The CsrfExtewnsion is included with Lagan. 420 | 421 | 422 | #### templates (directory) #### 423 | 424 | This directory contains the template files (except the property templates). The subdirectory *admin* contains all the template files for the admin environment. 425 | Bonus feature: the subdirectory *static* contains the template files for static pages and a 404 page. Static pages display if the route name matches their name, and no other route for this name exists. Convenient! 426 | 427 | 428 | #### vendor (directory) #### 429 | 430 | Created by [Composer](https://getcomposer.org/) when installing the project dependencies. 431 | 432 | 433 | #### config.php (file) #### 434 | 435 | The Composer script renames the *config_example.php* file to *config.php*. The *config.php* file is needed for a Lagan project to work. Remember to add the necessary details. 436 | 437 | 438 | #### setup.php (file) #### 439 | 440 | This is the setup file for your Lagan app and the unit tests. The setup.php file contains the configuration for RedBean, the Composer autoloader and the autoloader for Lagan models. 441 | 442 | 443 | Where does the name Lagan come from, and how do you pronounce it? 444 | ----------------------------------------------------------------- 445 | 446 | [River Lagan](https://en.wikipedia.org/wiki/River_Lagan) is a river that runs through [Belfast](https://en.wikipedia.org/wiki/Belfast). I lived in Belfast when I created Lagan. 447 | Lagan is pronounced /'laeg=n/ with stress on first syllable, /ae/ as in "cat" and /=/ as in the schwah or neutral "e" sound in English. (Eg "letter" = /'let=(r)/.) 448 | 449 | 450 | To do 451 | ----- 452 | 453 | There is a [Lagan wishlist on Trello](https://trello.com/b/szpUocBL). 454 | 455 | 456 | 457 | Lagan is a project of [Lútsen Stellingwerff](http://lutsen.net/). -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lagan/lagan", 3 | "type": "project", 4 | "description": "Lagan lets you create flexible content objects with a simple class, and manage them with a web interface.", 5 | "keywords": ["cms","content", "backend","slim","redbean","twig","validation"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Lútsen Stellingwerff", 10 | "email": "lutsenstellingwerff@gmail.com", 11 | "homepage": "http://lutsen.net", 12 | "role": "Developer" 13 | } 14 | ], 15 | "require-dev": { 16 | "phpunit/phpunit": "5.5.*" 17 | }, 18 | "require": { 19 | "php": ">=5.5.0", 20 | "gabordemooij/redbean": "^4.0", 21 | "slim/slim": "^3.0", 22 | "slim/twig-view": "^2.0", 23 | "slim/flash": "^0.2", 24 | "twig/twig": "^1.0", 25 | "tuupola/slim-basic-auth": "^2.0", 26 | "slim/csrf": "^0.8", 27 | 28 | "lagan/core": "^1.0", 29 | 30 | "lagan/property-fileselect": "^1.0", 31 | "lagan/property-hashid": "^1.0", 32 | "lagan/property-manytomany": "^1.0", 33 | "lagan/property-manytoone": "^1.0", 34 | "lagan/property-onetomany": "^1.0", 35 | "lagan/property-position": "^1.0", 36 | "lagan/property-slug": "^1.0", 37 | "lagan/property-string": "^1.0", 38 | "lagan/property-upload": "^1.0", 39 | 40 | "lagan/template-fileselect": "^1.0", 41 | "lagan/template-manytoone": "^1.0", 42 | "lagan/template-text": "^1.0", 43 | "lagan/template-textarea": "^1.0", 44 | "lagan/template-tomany": "^1.0", 45 | "lagan/template-upload": "^1.0" 46 | }, 47 | "scripts": { 48 | "post-update-cmd": [ 49 | "php -r \"// Create RedBean rb.php file\"", 50 | "php -r \"chdir('vendor/gabordemooij/redbean'); require('replica2.php');\"" 51 | ], 52 | "post-create-project-cmd": [ 53 | "php -r \"// Create cache directory\"", 54 | "php -r \"mkdir('cache', 0755);\"", 55 | "php -r \"// Rename config file\"", 56 | "php -r \"rename('config_example.php', 'config.php');\"", 57 | "php -r \"// Setting ROOT_PATH in config file\"", 58 | "php -r \"file_put_contents('config.php', str_replace('define(\\'ROOT_PATH\\', \\'\\')', 'define(\\'ROOT_PATH\\', \\''.__DIR__.'\\')', file_get_contents('config.php') ) );\"", 59 | "php -r \"echo PHP_EOL . ' Thank you for installing Lagan! ' . PHP_EOL . PHP_EOL;\"" 60 | ] 61 | } 62 | } -------------------------------------------------------------------------------- /config_example.php: -------------------------------------------------------------------------------- 1 | '', 14 | 'username' => '', 15 | 'password' => '', 16 | 'database' => '' 17 | ); 18 | 19 | /** 20 | * @var string[] An array with admin usernames (key) and their passwords (value). 21 | */ 22 | $users = array( 23 | 'admin' => 'password' 24 | ); 25 | 26 | /** 27 | * @var string[] An array of the paths protected by the user password combination. 28 | */ 29 | $protected = array( 30 | '/admin' 31 | ); 32 | 33 | /** 34 | * @const ERROR_REPORTING Enable or disable error reporting. 35 | */ 36 | define('ERROR_REPORTING', true); 37 | 38 | /** 39 | * @const ROOT_PATH The server path to the root directory of your Lagan app (Should not have a trailing slash). 40 | */ 41 | define('ROOT_PATH', ''); 42 | /** 43 | * @const APP_PATH The server path to the public directory of your Lagan app (Should not have a trailing slash). 44 | */ 45 | define('APP_PATH', ''); 46 | /** 47 | * @const APP_URL The URL of your Lagan app (Should not have a trailing slash). 48 | */ 49 | define('APP_URL', ''); 50 | ?> -------------------------------------------------------------------------------- /lagan-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 26 | 30 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /models/lagan/Crew.php: -------------------------------------------------------------------------------- 1 | type = 'crew'; 13 | 14 | // Description in admin interface 15 | $this->description = 'Crewmembers to man the Hoverkraft.'; 16 | 17 | $this->properties = [ 18 | // Always have a title 19 | [ 20 | 'name' => 'title', 21 | 'description' => 'Name', 22 | 'required' => true, 23 | 'searchable' => true, 24 | 'type' => '\Lagan\Property\Str', 25 | 'input' => 'text', 26 | 'validate' => 'minlength(3)' 27 | ], 28 | [ 29 | 'name' => 'bio', 30 | 'description' => 'Biography', 31 | 'searchable' => true, 32 | 'type' => '\Lagan\Property\Str', 33 | 'input' => 'textarea' 34 | ], 35 | [ 36 | 'name' => 'email', 37 | 'description' => 'Email address', 38 | 'searchable' => true, 39 | 'type' => '\Lagan\Property\Str', 40 | 'input' => 'text', 41 | 'validate' => 'emaildomain' 42 | ], 43 | [ 44 | 'name' => 'picture', 45 | 'description' => 'Image', 46 | 'required' => true, 47 | 'type' => '\Lagan\Property\Upload', 48 | 'directory' => '/uploads', // Directory relative to APP_PATH (no trailing slash) 49 | 'input' => 'upload', 50 | 'validate' => [ ['extension', 'allowed=jpeg,jpg,gif,png'], ['size', 'size=1M'] ] 51 | ], 52 | [ 53 | 'name' => 'hoverkraft', 54 | 'description' => 'Hoverkraft', 55 | 'required' => true, 56 | 'type' => '\Lagan\Property\Manytoone', 57 | 'input' => 'manytoone' 58 | ] 59 | ]; 60 | } 61 | 62 | } 63 | 64 | ?> 65 | -------------------------------------------------------------------------------- /models/lagan/Feature.php: -------------------------------------------------------------------------------- 1 | type = 'feature'; 13 | 14 | // Description in admin interface 15 | $this->description = 'Feastures the Hoverkrafts can have. Like a turbo, or a coffee machine.'; 16 | 17 | $this->properties = [ 18 | // Always have a title 19 | [ 20 | 'name' => 'title', 21 | 'description' => 'Name', 22 | 'required' => true, 23 | 'type' => '\Lagan\Property\Str', 24 | 'input' => 'text' 25 | ], 26 | [ 27 | 'name' => 'description', 28 | 'description' => 'Describe the feature', 29 | 'type' => '\Lagan\Property\Str', 30 | 'input' => 'textarea' 31 | ], 32 | [ 33 | 'name' => 'hoverkraft', 34 | 'description' => 'Hoverkrafts with this feature', 35 | 'required' => true, 36 | 'type' => '\Lagan\Property\Manytomany', 37 | 'input' => 'tomany' 38 | ] 39 | ]; 40 | } 41 | 42 | } 43 | 44 | ?> 45 | -------------------------------------------------------------------------------- /models/lagan/Hoverkraft.php: -------------------------------------------------------------------------------- 1 | type = 'hoverkraft'; 13 | 14 | // Description in admin interface 15 | $this->description = 'A hoverkraft is a very special vessel (and a great design angency).'; 16 | 17 | $this->properties = [ 18 | // Always have a title 19 | [ 20 | 'name' => 'title', 21 | 'description' => 'Title', 22 | 'required' => true, 23 | 'searchable' => true, 24 | 'type' => '\Lagan\Property\Str', 25 | 'input' => 'text' 26 | ], 27 | [ 28 | 'name' => 'description', 29 | 'description' => 'Describe the kraft', 30 | 'searchable' => true, 31 | 'type' => '\Lagan\Property\Str', 32 | 'input' => 'textarea' 33 | ], 34 | [ 35 | 'name' => 'picture', 36 | 'description' => 'Image', 37 | 'required' => true, 38 | 'type' => '\Lagan\Property\Fileselect', 39 | 'extensions' => 'jpeg,jpg,gif,png', // Allowed extensions 40 | 'directory' => '/files', // Directory relative to APP_PATH (no trailing slash) 41 | 'input' => 'fileselect' 42 | ], 43 | [ 44 | 'name' => 'position', 45 | 'description' => 'Order', 46 | 'autovalue' => true, 47 | 'type' => '\Lagan\Property\Position', 48 | 'input' => 'text' 49 | ], 50 | [ 51 | 'name' => 'slug', 52 | 'description' => 'Slug', 53 | 'autovalue' => true, 54 | 'type' => '\Lagan\Property\Slug', 55 | 'input' => 'text' 56 | ], 57 | [ 58 | 'name' => 'crew', 59 | 'description' => 'Crewmembers for this Hoverkraft', 60 | 'type' => '\Lagan\Property\Onetomany', 61 | 'input' => 'tomany' 62 | ], 63 | [ 64 | 'name' => 'feature', 65 | 'description' => 'Features this Hoverkraft has', 66 | 'type' => '\Lagan\Property\Manytomany', 67 | 'input' => 'tomany' 68 | ] 69 | ]; 70 | } 71 | 72 | } 73 | 74 | ?> 75 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | # Allow webfonts from differnet (sub)domains 2 | # (For example Typekit) 3 | 4 | 5 | Header set Access-Control-Allow-Origin "*" 6 | 7 | 8 | 9 | RewriteEngine On 10 | 11 | # Force www 12 | # - Exclude urls starting with loaclhost 13 | # - Check whether the Host value is not empty (in case of HTTP/1.0) 14 | # - Check for 2 dots or not (instead of checking for www, because that would break subdomains) 15 | # For the subdomain to work, in your subdomain folder should also be an .htaccess file. 16 | # In its simplest form, this could look something like: 17 | # 18 | # RewriteEngine on 19 | # RewriteBase / 20 | # 21 | # - Checks for HTTPS (%{HTTPS} is either on or off, so %{HTTPS}s is either ons or offs and in case of ons the s is matched) 22 | # - The substitution part of RewriteRule then just merges the information parts to a full URL 23 | RewriteCond %{HTTP_HOST} !^localhost 24 | RewriteCond %{HTTP_HOST} ^(.*)$ [NC] 25 | RewriteCond %{HTTP_HOST} !^(.*)\.(.*)\. [NC] 26 | RewriteCond %{HTTPS}s ^on(s)| 27 | RewriteRule ^ HTTP%1://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L] 28 | 29 | # To force httpS uncomment the 2 lines below 30 | # RewriteCond %{HTTPS} off 31 | # RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L,NE] 32 | 33 | RewriteCond %{REQUEST_FILENAME} !-f 34 | RewriteCond %{REQUEST_FILENAME} !-d 35 | RewriteRule ^ index.php [QSA,L] -------------------------------------------------------------------------------- /public/files/crew-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutsen/lagan/729fb9bdfcdc39a4a3bfebdea8a434627bc4a4d3/public/files/crew-1.jpg -------------------------------------------------------------------------------- /public/files/crew-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutsen/lagan/729fb9bdfcdc39a4a3bfebdea8a434627bc4a4d3/public/files/crew-2.jpg -------------------------------------------------------------------------------- /public/files/crew-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutsen/lagan/729fb9bdfcdc39a4a3bfebdea8a434627bc4a4d3/public/files/crew-3.jpg -------------------------------------------------------------------------------- /public/files/hoverkraft-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutsen/lagan/729fb9bdfcdc39a4a3bfebdea8a434627bc4a4d3/public/files/hoverkraft-1.jpg -------------------------------------------------------------------------------- /public/files/hoverkraft-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutsen/lagan/729fb9bdfcdc39a4a3bfebdea8a434627bc4a4d3/public/files/hoverkraft-2.jpg -------------------------------------------------------------------------------- /public/files/hoverkraft-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutsen/lagan/729fb9bdfcdc39a4a3bfebdea8a434627bc4a4d3/public/files/hoverkraft-3.jpg -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | [ 31 | 'displayErrorDetails' => ERROR_REPORTING 32 | ]]); 33 | 34 | $container = $app->getContainer(); 35 | 36 | // Register Slim flash messages 37 | $container['flash'] = function () { 38 | return new \Slim\Flash\Messages(); 39 | }; 40 | 41 | // Register Slim Framework CSRF protection middleware 42 | $container['csrf'] = function ($c) { 43 | // Changed $persistentTokenMode to true for easier Ajax calls 44 | // For default settings return new \Slim\Csrf\Guard; will do... 45 | return new \Slim\Csrf\Guard( 46 | 'csrf', // $prefix 47 | $storage, // &$storage 48 | null, // $failureCallable 49 | 200, // $storageLimit 50 | 16, // $strength 51 | true //$persistentTokenMode 52 | ); 53 | }; 54 | 55 | // Register CSRF protection middleware for all routes 56 | $app->add($container->get('csrf')); 57 | 58 | // Add HTTP Basic Authentication middleware 59 | $app->add(new \Slim\Middleware\HttpBasicAuthentication([ 60 | 'path' => $protected, // Defined in config.php 61 | 'secure' => true, 62 | 'relaxed' => ['localhost'], 63 | 'users' => $users // Defined in config.php 64 | ])); 65 | 66 | // Register Twig View helper 67 | $container['view'] = function ($c) { 68 | $view = new \Slim\Views\Twig([ 69 | ROOT_PATH.'/templates', 70 | APP_PATH.'/property-templates' 71 | ], 72 | [ 73 | 'cache' => ROOT_PATH.'/cache' 74 | ]); 75 | 76 | // Instantiate and add Slim specific extension 77 | $basePath = rtrim(str_ireplace('index.php', '', $c['request']->getUri()->getBasePath()), '/'); 78 | $view->addExtension(new \Slim\Views\TwigExtension($c['router'], $basePath)); 79 | $view->addExtension( new CsrfExtension( $c->get('csrf') ) ); // CSRF protection 80 | 81 | // General variables to render views 82 | $view->offsetSet('app_url', APP_URL); 83 | $view->offsetSet('page_url', APP_URL . $c['request']->getUri()->getPath()); 84 | 85 | return $view; 86 | }; 87 | 88 | 89 | 90 | // ### ROUTES ### // 91 | 92 | // Include all the route files. 93 | $static = ROOT_PATH.'/routes/static.php'; 94 | $routeFiles = glob(ROOT_PATH.'/routes/*.php'); 95 | 96 | foreach( $routeFiles as $routeFile ) { 97 | if ( $routeFile !== ROOT_PATH.'/routes/static.php' ) { 98 | require_once $routeFile; 99 | } 100 | } 101 | 102 | // The route for static pages has to come last to work. 103 | if ( file_exists($static) ) { 104 | require_once $static; 105 | } 106 | 107 | $app->run(); 108 | 109 | ?> -------------------------------------------------------------------------------- /routes/admin.php: -------------------------------------------------------------------------------- 1 | withStatus(302)->withHeader( 21 | 'Location', 22 | $container->get('router')->pathFor( 'listbeans', [ 'beantype' => $args['beantype'] ] ) 23 | ); 24 | } else { 25 | return $response->withStatus(302)->withHeader( 26 | 'Location', 27 | $container->get('router')->pathFor( 'getbean', [ 'beantype' => $args['beantype'], 'id' => $bean->id ] ) 28 | ); 29 | } 30 | } 31 | 32 | // Users need to authenticate with HTTP Basic Authentication middleware 33 | $app->group('/admin', function () { 34 | 35 | $this->get('[/]', function ($request, $response, $args) { 36 | $beantypes = getBeantypes(); 37 | 38 | foreach ($beantypes as $beantype) { 39 | $dashboard[$beantype]['name'] = $beantype; 40 | $dashboard[$beantype]['total'] = \R::count( $beantype ); 41 | $dashboard[$beantype]['created'] = \R::findOne( $beantype, ' ORDER BY created DESC ' ); 42 | $dashboard[$beantype]['modified'] = \R::findOne( $beantype, ' ORDER BY modified DESC ' ); 43 | 44 | $c = setupBeanModel( $beantype ); 45 | $dashboard[$beantype]['description'] = $c->description; 46 | } 47 | 48 | return $this->view->render( $response, 'admin/index.html', [ 49 | 'dashboard' => $dashboard, 50 | 'beantypes' => $beantypes 51 | ] ); 52 | })->setName('admin'); 53 | 54 | // Route of a certain type of bean 55 | $this->group('/{beantype}', function () { 56 | 57 | // List 58 | $this->get('[/]', function ($request, $response, $args) { 59 | $data['flash'] = $this->flash->getMessages(); 60 | 61 | try { 62 | 63 | $c = setupBeanModel( $args['beantype'] ); 64 | 65 | $data['beantype'] = $args['beantype']; 66 | $data['description'] = $c->description; 67 | $data['beantypes'] = getBeantypes(); 68 | $data['properties'] = $c->properties; 69 | 70 | $query = $request->getParams(); 71 | 72 | foreach($c->properties as $property) { 73 | // Sort "absolute" positions, not related to manytoone parent. 74 | if ( $property['type'] === '\\Lagan\\Property\\Position' && !isset( $property['manytoone'] ) ) { 75 | 76 | // Set default sorting 77 | if ( !$query['sort'] ) { 78 | $query['sort'] = $property['name'].'*asc'; 79 | } 80 | 81 | // Set sorting in web interface 82 | $data['position'] = $property; 83 | 84 | break; 85 | } 86 | } 87 | 88 | // Set default sorting if not set yet 89 | if ( !$query['sort'] ) { 90 | $query['sort'] = 'title*asc'; 91 | } 92 | 93 | // Search 94 | $search = new \Lagan\Search( $args['beantype'] ); 95 | $data['search'] = $search->find( $query ); 96 | 97 | if ( $request->getParam('*has') ) { 98 | $data['query'] = $request->getParam('*has'); // Output in title, needs work to work with all kinds of search queries 99 | } 100 | 101 | } catch (Exception $e) { 102 | $data['flash']['error'][] = $e->getMessage(); 103 | } 104 | 105 | // Show list of items 106 | return $this->view->render($response, 'admin/beans.html', $data); 107 | 108 | })->setName('listbeans'); 109 | 110 | // Form to add new bean 111 | $this->get('/add', function ($request, $response, $args) { 112 | $c = setupBeanModel( $args['beantype'] ); 113 | $c->populateProperties(); 114 | 115 | // Show form 116 | return $this->view->render($response, 'admin/bean.html', [ 117 | 'method' => 'post', 118 | 'beantype' => $args['beantype'], 119 | 'beanproperties' => $c->properties, 120 | 'flash' => $this->flash->getMessages(), 121 | 'beantypes' => getBeantypes() 122 | ]); 123 | })->setName('addbean'); 124 | 125 | // View existing bean 126 | $this->get('/{id}', function ($request, $response, $args) { 127 | $c = setupBeanModel( $args['beantype'] ); 128 | $c->populateProperties( $args['id'] ); 129 | 130 | // Show populated form 131 | return $this->view->render($response, 'admin/bean.html', [ 132 | 'method' => 'put', 133 | 'beantype' => $args['beantype'], 134 | 'beanproperties' => $c->properties, 135 | 'bean' => $c->read( $args['id'] ), 136 | 'flash' => $this->flash->getMessages(), 137 | 'beantypes' => getBeantypes() 138 | ]); 139 | })->setName('getbean'); 140 | 141 | // Add 142 | $this->post('[/]', function ($request, $response, $args) { 143 | $c = setupBeanModel( $args['beantype'] ); 144 | $data = $request->getParsedBody(); 145 | 146 | try { 147 | $bean = $c->create( $data ); 148 | 149 | // Redirect to overview or populated form 150 | $this->flash->addMessage( 'success', $bean->title.' is added.' ); 151 | return redirectAfterSave($this, $bean, $data, $response, $args); 152 | } catch (Exception $e) { 153 | $this->flash->addMessage( 'error', $e->getMessage() ); 154 | return $response->withStatus(302)->withHeader( 155 | 'Location', 156 | $this->get('router')->pathFor( 'addbean', [ 'beantype' => $args['beantype'] ]) 157 | ); 158 | } 159 | })->setName('postbean'); 160 | 161 | // Update 162 | $this->put('/{id}', function ($request, $response, $args) { 163 | $c = setupBeanModel( $args['beantype'] ); 164 | $data = $request->getParsedBody(); 165 | 166 | try { 167 | $bean = $c->update( $data , $args['id'] ); 168 | 169 | // Redirect to overview or populated form 170 | $this->flash->addMessage( 'success', $bean->title.' is updated.' ); 171 | return redirectAfterSave($this, $bean, $data, $response, $args); 172 | } catch (Exception $e) { 173 | $this->flash->addMessage( 'error', $e->getMessage() ); 174 | return $response->withStatus(302)->withHeader( 175 | 'Location', 176 | $this->get('router')->pathFor( 'getbean', [ 'beantype' => $args['beantype'], 'id' => $args['id'] ] ) 177 | ); 178 | } 179 | })->setName('putbean'); 180 | 181 | // Delete 182 | $this->delete('/{id}', function ($request, $response, $args) { 183 | $c = setupBeanModel( $args['beantype'] ); 184 | 185 | try { 186 | $c->delete( $args['id'] ); 187 | $this->flash->addMessage( 'success', 'The '.$args['beantype'].' is deleted.' ); 188 | } catch (Exception $e) { 189 | $this->flash->addMessage( 'error', $e->getMessage() ); 190 | } 191 | return $response->withStatus(302)->withHeader( 192 | 'Location', 193 | $this->get('router')->pathFor( 'listbeans', [ 'beantype' => $args['beantype'] ]) 194 | ); 195 | })->setName('deletebean'); 196 | 197 | }); 198 | 199 | }); 200 | 201 | ?> -------------------------------------------------------------------------------- /routes/functions.php: -------------------------------------------------------------------------------- 1 | $value) { 33 | $beantypes[$key] = strtolower( substr( 34 | $value, 35 | strlen(ROOT_PATH. '/models/lagan/'), 36 | strlen($value) - strlen(ROOT_PATH. '/models/lagan/') - 4 37 | ) ); 38 | } 39 | 40 | return $beantypes; 41 | } 42 | 43 | ?> -------------------------------------------------------------------------------- /routes/public.php: -------------------------------------------------------------------------------- 1 | get('/', function ($request, $response, $args) { 11 | $hoverkraft = new \Lagan\Model\Hoverkraft; 12 | 13 | // Show list of Hoverkrafts 14 | return $this->view->render( 15 | $response, 'public/index.html', 16 | [ 'hoverkrafts' => $hoverkraft->read() ] 17 | ); 18 | }); 19 | 20 | // Search 21 | $app->get('/hoverkraft/search', function ($request, $response, $args) { 22 | $search = new \Lagan\Search('hoverkraft'); 23 | 24 | return $this->view->render( 25 | $response, 26 | 'public/search.html', 27 | [ 28 | 'search' => $search->find( $request->getParams() ), 29 | 'query' => $request->getParam('*has') 30 | ] 31 | ); 32 | }); 33 | 34 | // Show one Hoverkraft 35 | $app->get('/hoverkraft/{slug}', function ($request, $response, $args) { 36 | $hoverkraft = new \Lagan\Model\Hoverkraft; 37 | 38 | return $this->view->render( 39 | $response, 40 | 'public/hoverkraft.html', 41 | [ 'hoverkraft' => $hoverkraft->read( $args['slug'], 'slug' ) ] 42 | ); 43 | })->setName('hoverkraft'); 44 | 45 | ?> -------------------------------------------------------------------------------- /routes/static.php: -------------------------------------------------------------------------------- 1 | get('/{slug}', function ($request, $response, $args) { 11 | 12 | $slug = str_replace(array('../','./'), '', $args['slug']); // remove parent path components if request is trying to be sneaky 13 | 14 | if (file_exists(ROOT_PATH.'/templates/static/'.$slug.'.html')) { 15 | return $this->view->render($response, 'static/'.$slug.'.html'); 16 | } else { 17 | return $this->view->render($response, 'static/404.html')->withStatus(404); 18 | } 19 | }); 20 | 21 | ?> -------------------------------------------------------------------------------- /setup.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/admin/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ title }} - Lagan Admin 9 | 10 | {% block head %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% endblock head %} 20 | 21 | 22 | 153 | 154 | 155 | 156 | 157 | 175 | 176 |
177 |
178 | 179 | 189 | 190 |
191 | 192 | {% if flash.error or flash.success %} 193 |
194 | 195 | {% if flash.error %}{{ flash.error[0] }}{% else %}{{ flash.success[0] }}{% endif %} 196 |
197 | {% endif %} 198 | 199 | {% block content %} 200 | {# Content from child template #} 201 | {% endblock content %} 202 |
203 | 204 |
205 |
206 | 207 | {% block javascript %} 208 | 209 | 210 | 211 | 212 | {% endblock javascript %} 213 | 214 | 215 | -------------------------------------------------------------------------------- /templates/admin/bean.html: -------------------------------------------------------------------------------- 1 | {% if bean %} 2 | {% set title = bean.title %} 3 | {% else %} 4 | {% set title = 'New ' ~ beantype %} 5 | {% endif %} 6 | 7 | {% extends "admin/base.html" %} 8 | 9 | {% block content %} 10 | 11 |

{% if bean %}{{ bean.title }} ({{ beantype }}){% else %}New {{ beantype }}{% endif %}

12 | 13 | {% if bean %} 14 |
15 |
16 |

Created on {{ bean.created }}, last modified on {{ bean.modified }}.

17 |
18 |
19 | {% endif %} 20 | 21 |
22 | 23 | 24 | 25 | 26 | {% for property in beanproperties %} 27 | {% include property.input ~ '/' ~ property.input ~ '.html' %} 28 | {% endfor %} 29 |
30 |
31 | 32 | 33 | 34 |
35 |
36 |
37 | 38 | {% endblock content %} 39 | 40 | {% block javascript %} 41 | {{ parent() }} 42 | 43 | 44 | 63 | {% endblock javascript %} -------------------------------------------------------------------------------- /templates/admin/beans.html: -------------------------------------------------------------------------------- 1 | {% if query|length > 0 %} 2 | {% set title = beantype ~ ': search results for "' ~ query ~ '"' %} 3 | {% else %} 4 | {% set title = beantype %} 5 | {% endif %} 6 | 7 | {% extends "admin/base.html" %} 8 | 9 | {% block content %} 10 | 11 |

{{ beantype }}{% if query|length > 0 %}: search results for "{{ query }}"{% endif %}

12 | 13 |
14 | 15 |
16 |

{{ description }}

17 |
18 | 19 |
20 |
21 |
22 | 23 |
24 | 25 |
26 |
27 | 28 |
29 | Add new {{ beantype }} 30 |
31 | 32 |
33 | 34 | {% if search.result|length > 0 %} 35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | {% if position %} 43 | 44 | {% endif %} 45 | 46 | 47 | 48 | 49 | 50 | {% for bean in search.result %} 51 | 52 | 53 | 54 | 55 | {% if position %} 56 | 68 | {% endif %} 69 | 78 | 79 | 80 | {% endfor %} 81 | 82 |
{{ properties[0].description }}{{ properties[1].description }}{{ properties[2].description }}{{ position.description }}RemoveEdit
{{ bean[properties[0].name]|striptags[:75] }}{{ bean[properties[1].name]|striptags[:75] }}{{ bean[properties[2].name]|striptags[:75] }} 57 |
58 | 59 | ↑↑ 60 | 61 | 62 | 63 | 64 | 65 | ↓↓ 66 |
67 |
70 |
71 | 72 | 73 | 74 | 75 | 76 |
77 |
Edit
83 |
84 | {% else %} 85 |
86 | {% if search.query %} 87 |

There is no {{ beantype }} matching your search. You can add one.

88 | {% else %} 89 |

There is no {{ beantype }} yet. You can add one.

90 | {% endif %} 91 | {% endif %} 92 | 93 | {% if search.pages and search.pages > 1 %} 94 | 101 | {% endif %} 102 | 103 | {% endblock content %} 104 | 105 | {% block javascript %} 106 | {{ parent() }} 107 | 108 | 149 | {% endblock javascript %} -------------------------------------------------------------------------------- /templates/admin/index.html: -------------------------------------------------------------------------------- 1 | {% set title = 'Home' %} 2 | 3 | {% extends "admin/base.html" %} 4 | 5 | {% block content %} 6 | 7 |

Lagan admin

8 | 9 | {% for beantype in dashboard %} 10 | 11 | {% if (loop.index - 1) is divisible by(3) %} 12 |
13 | {% endif %} 14 | 15 |
16 |
17 |
18 |

19 | {{ beantype.name }} 20 |

21 |
22 | 23 |
{{ beantype.description }}
24 | 25 |
    26 |
  • 27 | Total entries: {{ beantype.total }} 28 |
  • 29 | {% if beantype.modified %} 30 |
  • 31 | Latest update: {{ beantype.modified.title }} on {{ beantype.modified.modified }} 32 |
  • 33 | {% endif %} 34 | {% if beantype.created %} 35 |
  • 36 | Newest entry: {{ beantype.created.title }} on {{ beantype.created.created }} 37 |
  • 38 | {% endif %} 39 |
40 | 41 | 51 | 52 |
53 |
54 | 55 | {% if loop.index is divisible by(3) or loop.index == dashboard|length %} 56 |
57 | {% endif %} 58 | 59 | {% endfor %} 60 | 61 |
62 |

Built with Lagan: any content, with a backend

63 | 64 | {% endblock content %} -------------------------------------------------------------------------------- /templates/public/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ title }} - Lagan example site 8 | 9 | {% block head %} 10 | 11 | {% endblock head %} 12 | 13 | 14 | 15 | 16 |
17 | 18 | {% block content %} 19 | {# Content from child template #} 20 | {% endblock content %} 21 | 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /templates/public/hoverkraft.html: -------------------------------------------------------------------------------- 1 | {% set title = hoverkraft.title %} 2 | 3 | {% extends "public/base.html" %} 4 | 5 | {% block content %} 6 | 7 | 10 | 11 |
12 |
13 |
14 | {{ hoverkraft.title }} 15 |
16 |
17 |
18 |

{{ hoverkraft.description }}

19 |
20 |
21 | 22 |

Crew

23 | 24 | {% if hoverkraft.crew|length > 0 %} 25 |
26 | {% for member in hoverkraft.crew %} 27 |
28 |
29 | {{ member.title }} 30 |
31 |

{{ member.title }}

32 |

{{ member.bio }}

33 |
34 |
35 |
36 | {% if loop.index % 4 == 0 %} 37 |
38 |
39 | {% endif %} 40 | {% endfor %} 41 |
42 | {% else %} 43 |
44 |

Sorry, there is no crew for this Hoverkraft...

45 | {% endif %} 46 | 47 |

Features

48 | 49 | {% if hoverkraft.feature|length > 0 %} 50 |
51 |
52 | {% for feature in hoverkraft.feature %} 53 |

{{ feature.title }}

54 |

{{ feature.description }}

55 | {% endfor %} 56 |
57 |
58 | {% else %} 59 |
60 |

Sorry, this Hoverkraft has no features...

61 | {% endif %} 62 | 63 | {% endblock content %} -------------------------------------------------------------------------------- /templates/public/index.html: -------------------------------------------------------------------------------- 1 | {% set title = 'Our Hoverkrafts' %} 2 | 3 | {% extends "public/base.html" %} 4 | 5 | {% block content %} 6 | 7 | 10 | 11 |
12 |
13 | 14 |
15 | 16 |
17 | 18 |
19 | 20 |

We have a lovely selection of Hoverkrafts. Check them out and meet their crew.

21 | 22 | {% if hoverkrafts|length > 0 %} 23 |
24 | {% for hoverkraft in hoverkrafts %} 25 |
26 |
27 | {{ hoverkraft.title }} 28 |
29 |

{{ hoverkraft.title }}

30 |

{{ hoverkraft.description }}

31 |

Check out this Hoverkraft

32 |
33 |
34 |
35 | {% if loop.index % 3 == 0 %} 36 |
37 |
38 | {% endif %} 39 | {% endfor %} 40 |
41 | {% else %} 42 |
43 |

Sorry, we're out of Hoverkrafts right now...

44 | {% endif %} 45 | 46 | {% endblock content %} -------------------------------------------------------------------------------- /templates/public/search.html: -------------------------------------------------------------------------------- 1 | {% set title = 'Search results for "' ~ query ~ '"' %} 2 | 3 | {% extends "public/base.html" %} 4 | 5 | {% block content %} 6 | 7 | 10 | 11 |
12 |
13 | 14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 | {% if search.result|length > 0 %} 22 | {% for hoverkraft in search.result %} 23 |
24 |
25 | 26 | {{ hoverkraft.title }} 27 | 28 |
29 |
30 | 31 |

{{ hoverkraft.title }}

32 |
33 | {{ hoverkraft.description }} 34 |
35 |
36 | {% endfor %} 37 | {% else %} 38 |
39 |

Sorry, your query doesn't match any results...

40 | {% endif %} 41 | 42 | {% if search.pages and search.pages > 1 %} 43 | 50 | {% endif %} 51 | 52 | {% endblock content %} -------------------------------------------------------------------------------- /templates/static/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found :( 6 | 141 | 142 | 143 |
144 |

Niet gevonden :(

145 |

Sorry, de pagina die je probeert te bekijken bestaat niet.

146 |

Dit komt waarschijnlijk door:

147 | 151 | 154 | 155 |
156 | 157 | -------------------------------------------------------------------------------- /templates/static/hello-world.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Hello World! 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |

Hello World!

17 |

This is a static page example. It displays if the route name matches the template name, and no other route with this name is defined. Convenient!

18 |
19 |
20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/LaganTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 38 | $beancount + 1, 39 | R::count( strtolower( $beantype ) ) 40 | ); 41 | } 42 | 43 | } 44 | 45 | // Create 46 | 47 | /** 48 | * @depends testSetup 49 | */ 50 | public function testCreate() { 51 | echo PHP_EOL; 52 | 53 | // Loop through all Lagan models 54 | $beantypes = getBeantypes(); 55 | 56 | foreach ( $beantypes as $beantype ) { 57 | $beancount = R::count( strtolower( $beantype ) ); 58 | $beans[ $beantype ] = createBean( $beantype ); 59 | // Create another bean so we can see something in the DB 60 | createBean( $beantype ); 61 | $this->assertEquals( 62 | $beancount + 2, 63 | R::count( strtolower( $beantype ) ) 64 | ); 65 | } 66 | 67 | return $beans; 68 | } 69 | 70 | // Read single 71 | 72 | /** 73 | * @depends testCreate 74 | */ 75 | public function testReadOne( $beans ) { 76 | echo PHP_EOL; 77 | 78 | // Loop through all Lagan models 79 | foreach ( $beans as $beantype => $bean ) { 80 | 81 | $c = setupBeanModel( $beantype ); 82 | $beans[ $beantype ] = $c->read( $bean->id, 'id' ); 83 | echo 'Bean ' . $bean->id . ' of ' . $beantype . ' read.' . PHP_EOL; 84 | 85 | } 86 | 87 | return $beans; 88 | } 89 | 90 | // Read all 91 | public function testReadAll() { 92 | echo PHP_EOL; 93 | 94 | // Loop through all Lagan models 95 | $beantypes = getBeantypes(); 96 | foreach ( $beantypes as $beantype ) { 97 | 98 | $c = setupBeanModel( $beantype ); 99 | $beans = $c->read(); 100 | echo 'All beans of ' . $beantype . ' read.' . PHP_EOL; 101 | 102 | } 103 | } 104 | 105 | // Update 106 | 107 | /** 108 | * @depends testReadOne 109 | */ 110 | public function testUpdate( $beans ) { 111 | echo PHP_EOL; 112 | 113 | // Loop through all Lagan models 114 | foreach ( $beans as $beantype => $bean ) { 115 | 116 | $c = setupBeanModel( $beantype ); 117 | $data = createContent( $c ); 118 | $beans[ $beantype ] = $c->update( $data, $bean->id ); 119 | 120 | $this->assertFalse( $bean->title == $beans[ $beantype ]->title ); // Title is random string 121 | echo 'Bean ' . $bean->id . ' of ' . $beantype . ' updated.' . PHP_EOL; 122 | 123 | } 124 | 125 | return $beans; 126 | } 127 | 128 | // Delete 129 | 130 | /** 131 | * @depends testUpdate 132 | */ 133 | public function testDelete( $beans ) {// Loop through all Lagan models 134 | echo PHP_EOL; 135 | 136 | // Loop through all Lagan models 137 | foreach ( $beans as $beantype => $bean ) { 138 | 139 | $beancount = R::count( strtolower( $beantype ) ); 140 | $c = setupBeanModel( $beantype ); 141 | $c->delete( $bean->id ); 142 | $this->assertEquals( 143 | $beancount - 1, 144 | R::count( strtolower( $beantype ) ) 145 | ); 146 | echo 'Bean ' . $bean->id . ' of ' . $beantype . ' deleted.' . PHP_EOL; 147 | 148 | } 149 | } 150 | 151 | } 152 | 153 | ?> -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | Lagan Unit Tests 2 | ================ 3 | 4 | PHP Unit is installed by Composer. 5 | 6 | To run all tests: 7 | `$ vendor/bin/phpunit tests` 8 | 9 | 10 | 11 | What are we testing? 12 | -------------------- 13 | 14 | We're testing the Lagan project. It has quite a few dependencies, like the Lagan core and the different Lagan properties. 15 | 16 | The example Lagan Models should use all these dependencies. So by testing them we're testing the dependencies as well. -------------------------------------------------------------------------------- /tests/functions.php: -------------------------------------------------------------------------------- 1 | $length ) { 37 | return substr( $val, 0, $length ); 38 | } else { 39 | return generateRandomString( $length ); 40 | } 41 | } 42 | 43 | /** 44 | * Get part of string that is between brackets. 45 | * 46 | * @var string $str The input string. 47 | * 48 | * @return string The part of the input string between brackets. 49 | */ 50 | function betweenBrackets( $str ) { 51 | return trim( substr( $str, strpos( $str, '(' ) + 1, strpos( $str, ')' ) - 1 ) ); 52 | } 53 | 54 | /** 55 | * Create test content for a Lagan content object. 56 | * 57 | * @var object $object A Lagan content object. 58 | * 59 | * @return array An array with test content data similar to a $_POST array. 60 | */ 61 | function createContent( $object ) { 62 | $data = []; 63 | 64 | foreach ( $object->properties as $property ) { 65 | 66 | // Loop through all properties 67 | switch ( $property['type'] ) { 68 | 69 | // Value: File path 70 | case '\Lagan\Property\Fileselect': 71 | $val = '/files/hoverkraft-1.jpg'; 72 | break; 73 | 74 | // Value: The id of the Instagram post. 75 | // The Instagram embed code is stored in $property['name'].'_embed' 76 | case '\Lagan\Property\Instaembed': 77 | $val = 'BNq-AmcDoYp'; 78 | break; 79 | 80 | // Value: An array with id's of the objects the object with this property has a many-to-many relation with. 81 | case '\Lagan\Property\Manytomany': 82 | $bean = R::findOne( $property['name'] ); 83 | if ($bean) 84 | $val = [ $bean->id ]; 85 | break; 86 | 87 | // Value: The id of the object the object with this property has a many-to-one relation with. 88 | case '\Lagan\Property\Manytoone': 89 | $bean = R::findOne( $property['name'] ); 90 | if ($bean) 91 | $val = $bean->id; 92 | break; 93 | 94 | // Value: An array with id's of the objects the object with this property has a one-to-many relation with. 95 | case '\Lagan\Property\Onetomany': 96 | $bean = R::findOne( $property['name'] ); 97 | if ($bean) 98 | $val = [$bean->id]; 99 | break; 100 | 101 | // Value: The input position of the object with this property. 102 | case '\Lagan\Property\Position': 103 | $val = 0; 104 | break; 105 | 106 | // Value: The input string for the slug of the object with this property. 107 | case '\Lagan\Property\Slug': 108 | $val = 'sluggish'; 109 | break; 110 | 111 | // Value: The input string of this property. 112 | case '\Lagan\Property\Str': 113 | // Check validation 114 | if ( isset( $property['validate'] ) ) { 115 | $rules = array_map( 'trim', explode( '|', $property['validate'] ) ); 116 | // Sort array to make sure alpha rules are set before length rules 117 | sort($rules); 118 | // Set right string according to each validation rule 119 | foreach ($rules as $rule) { 120 | switch (true) { 121 | case $rule == 'alpha': 122 | $val = generateRandomString(8); 123 | break; 124 | 125 | case $rule == 'alphanumeric': 126 | $val = '123 '.generateRandomString(8); 127 | break; 128 | 129 | case $rule == 'alphanumhyphen': 130 | $val = '1-2_3 '.generateRandomString(8); 131 | break; 132 | 133 | case substr($rule, 0, 6) == 'length': 134 | $optioms = betweenBrackets( $rule ); 135 | $optioms = array_map( 'trim', explode( ',', $optioms ) ); 136 | $val = setStr( !isset($val) ? 'lorum' : $val , $optioms[0] ); 137 | break; 138 | 139 | case substr($rule, 0, 9) == 'minlength': 140 | $min = betweenBrackets( $rule ); 141 | $val = setStr( !isset($val) ? 'lorum' : $val, $min ); 142 | break; 143 | 144 | case substr($rule, 0, 9) == 'maxlength': 145 | $max = betweenBrackets( $rule ); 146 | $val = setStr( !isset($val) ? 'lorum' : $val, $max ); 147 | break; 148 | 149 | case $rule == 'fullname': 150 | $val = generateRandomString(6); 151 | $val .= ' '; 152 | $val .= generateRandomString(8); 153 | break; 154 | 155 | case $rule == 'number': 156 | $val = 19.99; 157 | break; 158 | 159 | case $rule == 'integer': 160 | $val = 19; 161 | break; 162 | 163 | case substr($rule, 0, 8) == 'lessthan': 164 | $options = betweenBrackets( $rule ); 165 | $optioms = array_map( 'trim', explode( ',', $optioms ) ); 166 | $val = $optioms[0] - 1; 167 | break; 168 | 169 | case substr($rule, 0, 11) == 'greaterthan': 170 | $options = betweenBrackets( $rule ); 171 | $optioms = array_map( 'trim', explode( ',', $optioms ) ); 172 | $val = $optioms[0] + 1; 173 | break; 174 | 175 | case substr($rule, 0, 7) == 'between': 176 | $options = betweenBrackets( $rule ); 177 | $optioms = array_map( 'trim', explode( ',', $optioms ) ); 178 | $val = $optioms[0] + 1; 179 | break; 180 | 181 | case $rule == 'email': 182 | $val = generateRandomString(6); 183 | $val .= '@'; 184 | $val .= generateRandomString(8); 185 | $val .= '.com'; 186 | break; 187 | 188 | case $rule == 'emaildomain': 189 | $val = generateRandomString(6); 190 | $val .= '@gmail.com'; 191 | break; 192 | 193 | case $rule == 'url': 194 | $val = 'ftp://www.'; 195 | $val .= generateRandomString(6); 196 | $val .= '.com'; 197 | break; 198 | 199 | case $rule == 'website': 200 | $val = 'http://www.'; 201 | $val .= generateRandomString(6); 202 | $val .= '.com'; 203 | break; 204 | 205 | case substr($rule, 0, 4) == 'date': 206 | $format = betweenBrackets( $rule ); 207 | $val = DateTime::createFromFormat( $format, '19-Dec-2016' ); 208 | break; 209 | 210 | case $rule == 'datetime': 211 | $val = '2016-12-19 14:34:00'; 212 | break; 213 | 214 | case $rule == 'time': 215 | $val = '14:34:00'; 216 | break; 217 | 218 | default: 219 | throw new \Exception( 'The validation rule "'.$rule.'" is not part of this test.' ); 220 | break; 221 | } 222 | } 223 | } else { 224 | $val = generateRandomString( rand(12, 24) ); 225 | } 226 | break; 227 | 228 | // Value: No value is submtted, instead $_FILES[ $property['name'] ] is used. 229 | case '\Lagan\Property\Upload': 230 | // Need to set $val 231 | $val = false; 232 | // Simulate file upload 233 | $tmp = APP_PATH.'/files/tmp_file_'.uniqid().'.jpg'; 234 | copy( APP_PATH.'/files/hoverkraft-1.jpg', $tmp ); 235 | $_FILES = array( 236 | $property['name'] => array( 237 | 'name' => 'hoverkraft-1.jpg', 238 | 'type' => 'image/jpeg', 239 | 'size' => 152000, 240 | 'tmp_name' => $tmp, 241 | 'error' => 0 242 | ) 243 | ); 244 | break; 245 | 246 | default: 247 | throw new \Exception( 'This property type is not in the createContent function.' ); 248 | 249 | } 250 | 251 | if ( isset($val) ) { 252 | $data[ $property['name'] ] = $val; 253 | unset($val); // Reset for new loop 254 | } 255 | 256 | } 257 | 258 | return $data; 259 | 260 | } 261 | 262 | /** 263 | * Create a redbean bean with test data. 264 | * 265 | * @var string $beantype The type of bean to create. 266 | * 267 | * @return bean The created bean. 268 | */ 269 | function createBean( $beantype ) { 270 | $c = setupBeanModel( $beantype ); 271 | $data = createContent( $c ); 272 | $bean = $c->create( $data ); 273 | echo 'Bean ' . $bean->id . ' of ' . $beantype . ' created.' . PHP_EOL; 274 | return $bean; 275 | } 276 | 277 | ?> -------------------------------------------------------------------------------- /twigextensions/CsrfExtension.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | */ 12 | 13 | class CsrfExtension extends \Twig_Extension implements Twig_Extension_GlobalsInterface { 14 | 15 | /** 16 | * @var \Slim\Csrf\Guard 17 | */ 18 | protected $csrf; 19 | 20 | public function __construct(\Slim\Csrf\Guard $csrf) 21 | { 22 | $this->csrf = $csrf; 23 | } 24 | 25 | public function getGlobals() 26 | { 27 | // CSRF token name and value 28 | $csrfNameKey = $this->csrf->getTokenNameKey(); 29 | $csrfValueKey = $this->csrf->getTokenValueKey(); 30 | $csrfName = $this->csrf->getTokenName(); 31 | $csrfValue = $this->csrf->getTokenValue(); 32 | 33 | return [ 34 | 'csrf' => [ 35 | 'keys' => [ 36 | 'name' => $csrfNameKey, 37 | 'value' => $csrfValueKey 38 | ], 39 | 'name' => $csrfName, 40 | 'value' => $csrfValue 41 | ] 42 | ]; 43 | } 44 | 45 | public function getName() 46 | { 47 | return 'slim/csrf'; 48 | } 49 | } 50 | 51 | ?> --------------------------------------------------------------------------------