├── .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 |
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 |
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 |