├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── doc ├── console │ └── commands.md ├── content │ ├── formats.md │ ├── markdown.md │ ├── property-handlers.md │ └── retrieving-content.md ├── controller │ ├── content.md │ ├── custom.md │ ├── format.md │ └── template.md ├── feature │ ├── helpers.md │ ├── sitemap.md │ └── twig.md └── more │ ├── about.md │ └── contribution.md └── src ├── Application.php ├── Behavior └── PropertyHandlerInterface.php ├── Config ├── Configurator.php ├── Definition │ └── PhpillipConfiguration.php └── Loader │ └── YamlConfigLoader.php ├── Console ├── Application.php ├── Command │ ├── BuildCommand.php │ ├── ExposeCommand.php │ ├── ServeCommand.php │ └── WatchCommand.php ├── EventListener │ └── SitemapListener.php └── Model │ ├── Builder.php │ ├── Logger.php │ └── Sitemap.php ├── Controller └── ContentController.php ├── Encoder ├── MarkdownDecoder.php └── YamlEncoder.php ├── EventListener ├── ContentConverterListener.php ├── LastModifiedListener.php └── TemplateListener.php ├── Model └── Paginator.php ├── PropertyHandler ├── CallbackPropertyHandler.php ├── DateTimePropertyHandler.php ├── IntegerPropertyHandler.php ├── LastModifiedPropertyHandler.php └── SlugPropertyHandler.php ├── Provider ├── AutoControllerProvider.php ├── ContentControllerServiceProvider.php ├── ContentServiceProvider.php ├── DecoderServiceProvider.php ├── HostServiceProvider.php ├── InformatorServiceProvider.php ├── ParsedownServiceProvider.php ├── PygmentsServiceProvider.php ├── SubscriberServiceProvider.php └── TwigServiceProvider.php ├── Resources ├── bin │ ├── console │ └── router.php └── views │ ├── rss.xml.twig │ └── sitemap.xml.twig ├── Routing └── Route.php ├── Service ├── ContentRepository.php ├── Informator.php ├── Parsedown.php └── Pygments.php └── Twig ├── MarkdownExtension.php └── PublicExtension.php /.gitignore: -------------------------------------------------------------------------------- 1 | # Composer 2 | /vendor/ 3 | composer.lock 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Thomas Jarrand 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![](http://phpillip.github.io/phpillip.svg) 2 | 3 | > Phpillip is [Hugo](https://gohugo.io/)'s cousin. 4 | 5 | ## What 6 | 7 | Phpillip is static website generator written in PHP and powered by [Silex](http://silex.sensiolabs.org/) and [Symfony components](http://symfony.com/doc/current/components/index.html). 8 | 9 | It basically dumps your Silex application to static HTML files in a `/dist` folder. 10 | 11 | The result directory is meant to be served by an HTTP Server like [apache](http://apache.org) and [nginx](http://www.nginx.com) or published to static website services like [Github Pages](https://pages.github.com/). 12 | 13 | *It's particularly fit for __blogging__, __documentation__ and __showcase__.* 14 | 15 | ## How 16 | 17 | Phpillip is a [Silex](http://silex.sensiolabs.org/) application. 18 | 19 | The __build process__: 20 | - Loop through all declared _routes_ in the Application 21 | - Load content associated with the route (if any) from file 22 | - Call each route with its content in a _Request_ 23 | - Dump the _Response_ content in a file 24 | 25 | It supports as many format as you need. 26 | 27 | It uses the powerful [Twig](http://twig.sensiolabs.org/) engine for templating. 28 | 29 | ## Why 30 | 31 | Phpillip is meant to be: 32 | 33 | - Highly __extensible__ 34 | - Friendly with __Symfony__ developers 35 | - Clear, simple and clean 36 | 37 | ## Getting started 38 | 39 | Get your static website: 40 | 41 | 1. Bootstrap a Phpillip project 42 | 2. Write your content 43 | 3. Declare your routes and controllers 44 | 4. Provide templates 45 | 5. Build the static website 46 | 47 | ### 1. Bootstrap 48 | 49 | To bootstrap a new [Phpillip](https://github.com/Phpillip/phpillip) project: 50 | 51 | ``` bash 52 | composer create-project phpillip/phpillip-standard my_app 53 | cd my_app 54 | ``` 55 | 56 | ### 2. Write content 57 | 58 | Write your content file `[my-content-slug].[format]` in `src/Resources/data/[my-content-type]/`: 59 | 60 | __Example__ `src/Resources/data/article/why-use-phpillip.md`: 61 | 62 | ``` 63 | --- 64 | title: Why use Phpillip? 65 | --- 66 | 67 | # Why use Phpillip 68 | 69 | Why not! 70 | ``` 71 | 72 | ### 3. Declare routes and controllers 73 | 74 | Phpillip is a Silex application, so you can declare a route and its controller [the same way you would in Silex](http://silex.sensiolabs.org/doc/usage.html#routing): 75 | 76 | A closure: 77 | 78 | ``` php 79 | $this->get('/', function () { return []; })->template('index.html.twig'); 80 | ``` 81 | 82 | Your own controller class in 'src/Controller': 83 | 84 | ``` php 85 | $this->get('/blog', 'Controller\\BlogController::index'); 86 | ``` 87 | 88 | A controller service (here the Phpillip content controller service): 89 | 90 | ``` php 91 | $this->get('/blog/{post}', 'content.controller:show')->content('post'); 92 | ``` 93 | 94 | Phpillip gives you [many helpers](doc/feature/helpers.md) to automate content loading for your routes. 95 | 96 | ### 4. Provide templates 97 | 98 | Write the Twig templates corresponding to your routes and controllers in `src/Resources/views/` 99 | 100 | If you use the default Phpillip routes and controller, you'll need to provide: 101 | 102 | The list template: 103 | 104 | * _File:_ `[my-content-type]/index.html.twig`. 105 | * _Variables:_ An array of contents, named `[content-type]s`. 106 | 107 | ``` twig 108 | {% extends 'base.html.twig' %} 109 | {% block content %} 110 | {% for article in articles %} 111 | 112 | {{ article.title }} 113 | 114 | {% endfor %} 115 | {% endblock %} 116 | ``` 117 | 118 | The single content page template: 119 | 120 | * _File:_ `[my-content-type]/show.html.twig`. 121 | * _Variables:_ The content as an associative array, named `[content-type]`. 122 | 123 | ``` twig 124 | {% extends 'base.html.twig' %} 125 | {% block content %} 126 | {{ article.content }} 127 | {% endblock %} 128 | ``` 129 | 130 | ### 5. Build 131 | 132 | Build the static files to `/dist` with the Phpillip build command: 133 | 134 | bin/console phpillip:build 135 | 136 | ![](http://phpillip.github.io/build.gif) 137 | 138 | You're done! 139 | 140 | ## Going further: 141 | 142 | About Phpillip's __features__: 143 | - [Helpers: Param Converters and other route shortcuts](doc/feature/helpers.md) 144 | - [Sitemap](doc/feature/sitemap.md) 145 | 146 | About __content__: 147 | 148 | - [Supported formats](doc/content/formats.md) 149 | - [Markdown](doc/content/markdown.md) 150 | - [Retrieving content](doc/content/retrieving-content.md) 151 | - [Property handlers](doc/content/property-handlers.md) 152 | 153 | About __controllers__: 154 | 155 | - [Phpillip's default content controller](doc/controller/content.md) 156 | - [Custom controller classes](doc/controller/custom.md) 157 | - [Specifying output format](doc/controller/format.md) 158 | - [Template Resolution](doc/controller/template.md) 159 | 160 | About the __console__: 161 | 162 | - [Phpillip's Console](doc/console/commands.md) 163 | 164 | ## Contribution 165 | 166 | Any kind of [contribution](doc/more/contribution.md) is very welcome! 167 | 168 | ## Directory structure 169 | 170 | ``` 171 | # Sources directory 172 | src/ 173 | # Your Silex Application in which your declare routes, services, ... 174 | Application.php 175 | 176 | # Your controller classes (optional) 177 | # This is only a recommandation, you can put controllers wherever you like 178 | /Controller 179 | MyController.php 180 | 181 | # Resources 182 | /Resources 183 | 184 | # Configuration files directory 185 | config/ 186 | # Phpillip configuration 187 | config.yml 188 | 189 | # Content directory 190 | data/ 191 | # Create a directory for each content type 192 | post/ 193 | # Your 'post' contents goes here 194 | my-first-post.md 195 | a-post-in-json.json 196 | 197 | # Public directory 198 | public/ 199 | # All public directory content will be exposed in 'dist' 200 | css/ 201 | style.css 202 | 203 | # Views directory 204 | views/ 205 | # Your twig templates 206 | base.html.twig 207 | blog/ 208 | index.html.twig 209 | show.html.twig 210 | 211 | # Destination directory 212 | dist/ 213 | # The static files will be dumped in here 214 | 215 | ``` 216 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpillip/phpillip", 3 | "license": "MIT", 4 | "type": "library", 5 | "description": "PHP Static Site Generator", 6 | "homepage": "http://phpillip.github.io/", 7 | "authors": [ 8 | { 9 | "name": "Thomas Jarrand", 10 | "email": "thomas.jarrand@gmail.com", 11 | "homepage": "http://thomas.jarrand.fr", 12 | "role": "Developer" 13 | } 14 | ], 15 | "autoload": { 16 | "psr-4": { 17 | "Phpillip\\": "src" 18 | } 19 | }, 20 | "require": { 21 | "php": ">=5.6", 22 | "silex/silex": "~1.3", 23 | "symfony/config": "~2.7", 24 | "symfony/console": "~2.7", 25 | "symfony/filesystem": "~2.7", 26 | "symfony/finder": "~2.7", 27 | "symfony/process": "^2.7", 28 | "symfony/serializer": "~2.7", 29 | "symfony/twig-bridge": "^2.7", 30 | "symfony/yaml": "~2.7", 31 | "erusev/parsedown": "^1.5" 32 | }, 33 | "bin": [ 34 | "src/Resources/bin/console" 35 | ], 36 | "extra": { 37 | "branch-alias": { 38 | "dev-master": "1.0.x-dev" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /doc/console/commands.md: -------------------------------------------------------------------------------- 1 | # Phpillip's console 2 | 3 | > Phpillip uses Symfony console. So, in order to get the full list of available commands, just call `bin/console`. 4 | 5 | Phpillip provides 3 commands: 6 | 7 | ## Build 8 | 9 | Build the static files to `/dist`: 10 | 11 | __Usage:__ 12 | 13 | phpillip:build [options] [--] [] [] 14 | 15 | __Arguments:__ 16 | 17 | - `host`: What should be used as domain name for absolute url generation? 18 | - `destination`: Full path to destination directory 19 | 20 | __Options:__ 21 | 22 | - `--no-sitemap`: Don't build the sitemap 23 | - `--no-expose`: Don't expose the public directory after build 24 | 25 | Example: 26 | 27 | bin/console phpillip:build my-domain.com 28 | 29 | ## Watch 30 | 31 | Have Phpillip watch for any change in `/src` and rebuild the files automatically: 32 | 33 | __Usage:__ 34 | 35 | phpillip:watch [options] 36 | 37 | __Options:__ 38 | 39 | - `--period=PERIOD`: Set the polling period in seconds (default: 1) 40 | 41 | Example: 42 | 43 | bin/console phpillip:watch 44 | 45 | ## Serve 46 | 47 | You can also live-preview your website without building it by launching the Phpillip local PHP server: 48 | 49 | __Usage:__ 50 | 51 | phpillip:serve [options] [--] [
] 52 | 53 | __Arguments:__ 54 | 55 | - `address`: address:port (default: "127.0.0.1") 56 | 57 | __Options:__ 58 | 59 | - `-p`, `--port=PORT`: Address port number (default: "8080") 60 | 61 | Example: 62 | 63 | bin/console phpillip:serve 64 | 65 | Your website will be available at [http://localhost:8080](http://localhost:8080) 66 | 67 | __Note:__ 68 | 69 | It's a Symfony console so you can use shortcuts ;) 70 | 71 | # Build 72 | bin/console p:b 73 | 74 | # Watch 75 | bin/console p:w 76 | 77 | # Serve 78 | bin/console p:s 79 | 80 | -------------------------------------------------------------------------------- /doc/content/formats.md: -------------------------------------------------------------------------------- 1 | # Supported formats 2 | 3 | Phpillip already supports the following content formats: 4 | 5 | Format | Extension 6 | -------- | --------- 7 | Markdown | *.md 8 | YAML | *.yml 9 | JSON | *.json 10 | XML | *.xml 11 | 12 | ## Markdown 13 | 14 | The Markdown format gets a special treatment and [has its own documentation section](../content/markdown.md). 15 | 16 | ## Support your own format 17 | 18 | In Phpillip, the `decoder` service is responsible for parsing content. 19 | The decoder is a Symfony _Serializer_ filled with a Symfony _Decoder_ for each format. 20 | 21 | > If you're curious, have a look at `Phpillip\Provider\DecoderServiceProvider`. 22 | 23 | To support a new format, just create class that implement `Symfony\Component\Serializer\Encoder\DecoderInterface`: 24 | 25 | ``` php 26 | doSomethingWith($data); 51 | } 52 | } 53 | ``` 54 | 55 | And add it to Phpillip content encoders: 56 | 57 | ``` php 58 | $app['content_encoders'][] = new MyCustomDecoder(); 59 | ``` 60 | 61 | Files matching your custom format will now be parsed among the others. 62 | 63 | __Note:__ To know more about how Phpillip parses contents have a look at [property handlers](../content/property-handlers.md). 64 | -------------------------------------------------------------------------------- /doc/content/markdown.md: -------------------------------------------------------------------------------- 1 | # Markdown 2 | 3 | Phpillip is designed to parse Markdown as a first-choice content format. 4 | 5 | Markdown provides a cleanway of writing structured text and is easily converted to HTML. 6 | 7 | Phpillip relies on [Parsedown](http://parsedown.org/) for parsing Markdown, in the Markdown decoder. 8 | 9 | __Note:__ if you're not happy with it, you can always [setup you own encoder](../content/formats.md). 10 | 11 | ## The Markdown header 12 | 13 | YAML, JSON and XML are key/values formats, so they can be easily parsed as associative array. 14 | 15 | The Markdown can't. That's why Phpillip's markdown parser is a bit special. 16 | 17 | The content of the file is parsed and converted to HTML. 18 | The result is then stored into the `content` key of an associative array. 19 | 20 | You can define additional keys and values for content by writing a YAML header: 21 | 22 | ``` 23 | --- 24 | title: "My first blog post" 25 | description: "A fine blog post, you will like it." 26 | --- 27 | 28 | # My post title 29 | 30 | My content goes _here_! 31 | ``` 32 | 33 | This file would be decoded as the following array: 34 | 35 | ``` php 36 | [ 37 | 'title' => 'My first blog post', 38 | 'description' => 'A fine blog post, you will like it.', 39 | 'content' => '

My post title

My content goes here!

' 40 | ] 41 | ``` 42 | 43 | ## Syntax highlighting 44 | 45 | Thanks to Parsedown, Phpillip supports Github Flavored Markdown. 46 | That means you can define a language for your code blocks: 47 | 48 | ``` php 49 | $this->isPhp(); 50 | ``` 51 | 52 | 53 | ``` javascript 54 | this.isJavascript(); 55 | ``` 56 | 57 | Phpillip provides syntax highlighting for code block that define a language. He entrust [Pygments](http://pygments.org/), a python command line tool, to do the job. 58 | 59 | In order to get that feature, you'll need to install Pygments: 60 | 61 | pip install Pygments 62 | 63 | _Note:_ requires [Python](https://www.python.org/downloads/) 64 | -------------------------------------------------------------------------------- /doc/content/property-handlers.md: -------------------------------------------------------------------------------- 1 | # Property Handlers 2 | 3 | Property handlers are responsible for enriching parsed contents by providing automatic properties or casting properties as a certain type. 4 | 5 | Phpillip provides a default set of Property Handlers (see [Retrieving content](../content/retrieving-content.md)). 6 | 7 | And you're able to add your own to fit your needs! 8 | 9 | ## Create a custom property handler 10 | 11 | _Create a class_ that implements the `Phpillip\Behavior\PropertyHandlerInterface`: 12 | 13 | ``` php 14 | getProperty()]); 45 | } 46 | 47 | /** 48 | * Handle property 49 | * 50 | * @param mixed $value 51 | * @param array $context 52 | * 53 | * @return mixed 54 | */ 55 | public function handle($value, array $context) 56 | { 57 | return $this->doSomethingWith($value); 58 | } 59 | } 60 | ``` 61 | 62 | _Register your property handler_ in the Content Repository: 63 | 64 | ``` php 65 | $app['content_repository']->addPropertyHandler(new MyPropertyHandler()); 66 | ``` 67 | 68 | In this example, the __handle__ method will be called on every _my_property_ property when the content data _isSupported_. 69 | -------------------------------------------------------------------------------- /doc/content/retrieving-content.md: -------------------------------------------------------------------------------- 1 | # Retrieving content 2 | 3 | ## The content repository 4 | 5 | The content repository service is responsible for fetching your content. You'll find it in the Application under the *content_repository* key: 6 | 7 | ``` php 8 | $app['content_repository']; 9 | ``` 10 | 11 | When parsing a content, the repository returns an associative array with the following keys: 12 | 13 | Property | Presence | Description 14 | ------------- | ------------------------ | ----------------------------------- 15 | slug | Added if not provided | Slug, based on the source file name 16 | lastModified | Added if not provided | Last modification of the source file 17 | date | Parsed if provided | If a `date` property exists, parse it as DateTime 18 | weight | Parsed if provided | If a `date` property exists, parse it as DateTime 19 | content | Added for Markdown files | Content of the Markdown file, converted to HTML 20 | ... | Provided | Any other key present in the source file 21 | 22 | > Need more/differents properties? You can [add your own](../content/property-handlers.md) 23 | 24 | ## Fetching content 25 | 26 | ### Get a single content 27 | 28 | The `getContent` method expects a content type and a content name. It returns a single content: 29 | 30 | ``` php 31 | // Get a content matching `my-content.*` contents in 'src/Resources/data/foo': 32 | $app['content_repository']->getContent('foo', 'my-content'); 33 | 34 | // Result: 35 | [ 36 | 'slug' => 'my-content', 37 | 'lastModified' => DateTime, 38 | // ... Any other key present in the source file 39 | ] 40 | ``` 41 | 42 | ### Get all contents 43 | 44 | The `getContents` method expect a content type and return all its contents: 45 | 46 | ``` php 47 | // Get all contents in 'src/Resources/data/foo': 48 | $app['content_repository']->getContents('foo'); 49 | 50 | // Result: 51 | [ 52 | 'my-content' => ['slug' => 'my-content', 'lastModified' => DateTime, ...], 53 | 'my-other-content' => ['slug' => 'my-other-content', 'lastModified' => DateTime, ...], 54 | // ... 55 | ] 56 | ``` 57 | 58 | In a list, the contents are indexed by default by their source file name (a.k.a _slug_). 59 | 60 | ### Indexing and ordering contents 61 | 62 | Say you have a `post` content that contains a `date` property, you can get all the _posts_ indexed by date: 63 | 64 | ``` php 65 | // Get all contents in 'src/Resources/data/post' indexed by 'date': 66 | $app['content_repository']->getContents('post', 'date'); 67 | 68 | // Result: 69 | [ 70 | 1441836000 => ['date' => DateTime, 'slug' => 'my-first-post', ...], 71 | 1443474500 => ['date' => DateTime, 'slug' => 'my-second-post', ...], 72 | // ... 73 | ] 74 | ``` 75 | 76 | A third parameter is provided to sort the resulting array: 77 | 78 | ``` php 79 | // Get older post first ('date' ascending): 80 | $app['content_repository']->getContents('post', 'date', true); 81 | 82 | // Get latest post first ('date' descending): 83 | $app['content_repository']->getContents('post', 'date', false); 84 | ``` 85 | -------------------------------------------------------------------------------- /doc/controller/content.md: -------------------------------------------------------------------------------- 1 | # Content Controller 2 | 3 | Phpillip provides a default `ContentController` that supports 3 actions: 4 | 5 | - __show:__ Display a single content (suited for [single content](../feature/helpers.md#single-content)) 6 | - __list:__ Display a full list of content (suited for [content list](../feature/helpers.md#content-list)) 7 | - __page:__ Display one page of a paginated content list (suited for [pagination](../feature/helpers.md#pagination)) 8 | 9 | ## Show 10 | 11 | To register a controller that displays a single _achievement_: 12 | 13 | ``` php 14 | $this 15 | ->get('/achievements/{achievement}', 'content.controller:show') 16 | ->content('achievement'); 17 | ``` 18 | 19 | The expected template `achievement/show.html.twig` would receive the variable `achievement`. 20 | 21 | ## List 22 | 23 | To register a controller that displays all _achievements_: 24 | 25 | ``` php 26 | $this 27 | ->get('/achievements', 'content.controller:list') 28 | ->contents('achievement'); 29 | ``` 30 | 31 | The expected template `achievement/list.html.twig` would receive the variable `achievements`. 32 | 33 | ## Paginate 34 | 35 | To register a controller that paginates _achievements_: 36 | 37 | ``` php 38 | $this 39 | ->get('/achievements', 'content.controller:page') 40 | ->paginate('achievement'); 41 | ``` 42 | 43 | The expected template `achievement/page.html.twig` would receive the following variables: 44 | - `achievements`: Achievements for the current page 45 | - `page`: Index of the current page 46 | - `pages`: Total number of pages 47 | -------------------------------------------------------------------------------- /doc/controller/custom.md: -------------------------------------------------------------------------------- 1 | # Custom Controller 2 | 3 | You can declare your own controllers as classes: 4 | 5 | ``` php 6 | $app['content_repository']->getContents('product'), 31 | ]; 32 | } 33 | 34 | /** 35 | * Show a product 36 | * 37 | * @param Request $request 38 | * @param Application $app 39 | * @param string $reference 40 | * 41 | * @return array 42 | */ 43 | public function show(Request $request, Application $app, array $reference) 44 | { 45 | return [ 46 | 'products' => $app['content_repository']->getContent('product', $reference), 47 | ]; 48 | } 49 | } 50 | ``` 51 | 52 | Register your controller in the app: 53 | 54 | ``` php 55 | $this->get('/products', 'Controller\\ProductController:index'); 56 | $this->get('/product/{reference}', 'Controller\\ProductController:show'); 57 | ``` 58 | 59 | The expected template `achievement/list.html.twig` would receive the variable `achievements`. 60 | -------------------------------------------------------------------------------- /doc/controller/format.md: -------------------------------------------------------------------------------- 1 | # Route format 2 | 3 | Specifiyng the format of a route will determine the extention of the output file during the build. 4 | 5 | By default, all routes are treated as _HTML_ and therefore dumped as `.html` files. 6 | 7 | Phpillip rely on the _Response_ `Content-Type` header to determine the format of a route. 8 | 9 | To control the output format of a route, you just need to configure the _Response_ with the desired content type. 10 | 11 | There are 3 ways to do it: 12 | 13 | ## Do it, literally: 14 | 15 | ```php 16 | function () { 17 | // Will output a '.txt' file 18 | return new Response('Hello', 200, ['Content-Type' => 'text/plain']); 19 | } 20 | 21 | function () { 22 | // Will output a '.json' file 23 | return new JsonResponse($data); 24 | } 25 | ``` 26 | 27 | ## Set the *_format* attribute of the _Request_ 28 | 29 | In Symfony, _Response_ content type is by default determined by the format of the _Request_. 30 | 31 | So you can define the output format of a route by setting the _Request_ attribute `_format`. Phpillip will provide you with a `format` method on the route to do just that: 32 | 33 | ```php 34 | // Will output a '.txt' file 35 | $app->get('/hello')->format('txt'); 36 | ``` 37 | 38 | __Note__: Remember that the _Response_ expects a Mime-Type (e.g _text/html_) but the _Request_ expects a format (e.g. _html_). 39 | 40 | Finally you can set the format of the route by explicitly naming the file in the url pattern: 41 | 42 | ```php 43 | // Will output a '.json' file 44 | $app->get('/hello.json'); 45 | ``` 46 | 47 | # File name 48 | 49 | Additionaly, you can choose a custom name for the output file. 50 | 51 | With the `setFileName` method: 52 | 53 | ```php 54 | // Will output a '404.html' file 55 | $app->get('/404')->setFilename('404'); 56 | ``` 57 | 58 | Or directly in url pattern: 59 | 60 | ```php 61 | // Will output a 'feed.rss' file 62 | $app->get('/feed.rss'); 63 | ``` 64 | 65 | __Note:__ The default output file name is `index`. 66 | -------------------------------------------------------------------------------- /doc/controller/template.md: -------------------------------------------------------------------------------- 1 | # Template resolution 2 | 3 | Phpillip provides the same type of template resolution that you get in Symfony. 4 | When a Controller doesn't return a _Response_, Phpillip will try to create one by finding and rendering a matching template. 5 | 6 | ## For content routes 7 | 8 | If a route is declared as having a _content_, Phpillip will look for the template: `[content_type]/[show|list].[format].twig` 9 | 10 | ```php 11 | // For single content: 12 | $app->get('/blog/{post}', 'content.controller:show')->content('post'); 13 | 14 | // The template: 'src/Resources/views/post/show.html.twig' 15 | ``` 16 | 17 | 18 | ```php 19 | // For several contents: 20 | $app->get('/blog', 'content.controller:show')->paginate('post'); 21 | // or 22 | $app->get('/blog', 'content.controller:show')->contents('post'); 23 | 24 | // The template: 'src/Resources/views/post/list.html.twig' 25 | ``` 26 | 27 | ## For Class controllers 28 | 29 | If you declare you controller as a Class Controller: 30 | 31 | ```php 32 | $app->get('/blog', 'Acme\Controller\BlogController::index'); 33 | ``` 34 | 35 | Phpillip will look for the template `[ControllerName]/[actionName].[format].twig` (just like Symfony does). 36 | 37 | In our example: `src/Resources/views/Blog/index.html.twig` 38 | 39 | __Note:__ Phpillip looks for a twig template matching the format of your route. 40 | 41 | -------------------------------------------------------------------------------- /doc/feature/helpers.md: -------------------------------------------------------------------------------- 1 | # Route Helpers 2 | 3 | Phpillip's Route extends Silex's Route and provide an extra set of helpers. 4 | 5 | These helpers are designed to save you time by addressing common controller needs automatically: parameters to content conversion, pagination, template resolution... 6 | 7 | ## Content issues 8 | 9 | When building a route that depends on the content (e.g. `/blog/{article}`), Phpillip will need to call the route for each article content you have: _blog/my-first-post_, _blog/my-second-post_... 10 | 11 | That's why Phpillip provides helpers to _link_ your routes to your contents. 12 | 13 | ### Single content 14 | 15 | To tell Phpillip that your route vary over content, use the `content` method on the route: 16 | 17 | ``` php 18 | // For an 'article' content type: 19 | $app->get('/blog/{article}')->content('article'); 20 | ``` 21 | 22 | In this example, Phpillip will call the route for each article file found in `src/Resources/data/article`. 23 | 24 | The _content_ helper also acts as a param converter. 25 | 26 | When a _Request_ hits a route that is declared as having a content, Phpillip will try to fetch the content for you. 27 | 28 | In our previous example, calling the url `/blog/my-first-post` will match our route and Phpillip will look for a file named `src/Resources/data/article/my-first-post.*`. 29 | 30 | If such file exists, Phpillip will parse it as an associative array and set it as an `article` attribute in the _Request_. 31 | 32 | Of course, you can get the automatically fetched content in your controller: 33 | 34 | ``` php 35 | // As a parameter: 36 | function (array $article) { 37 | // Your content is $article 38 | } 39 | 40 | // And in the Request attributes: 41 | function (Request $request) { 42 | $article = $request->attributes->get('article'); 43 | } 44 | ``` 45 | 46 | ### Content list 47 | 48 | To tell Phpillip that your route is a _list_ of contents, use the `contents` methode on the route: 49 | 50 | ``` php 51 | // For an 'article' content type: 52 | $app->get('/blog')->contents('article'); 53 | ``` 54 | 55 | This only acts as a parameter converter. 56 | 57 | When a _Request_ hits a route that is declared as being a _list_ −in our example `/blog`− Phpillip will look for all files in `src/Resources/data/article`. Then it will parse each file as an associative array, store the results in an array and set it as an `articles` attribute in the _Request_. 58 | 59 | To get the automatically fetched contents in your controller: 60 | 61 | ``` php 62 | // As a parameter: 63 | function (array $articles) { 64 | // Your articles are in $articles 65 | } 66 | 67 | // And in the Request attributes: 68 | function (Request $request) { 69 | $articles = $request->attributes->get('articles'); 70 | } 71 | ``` 72 | 73 | By default, the contents are indexed numerically in the array and in the same order as they appear in the file system. 74 | 75 | You can define custom index and sorting in the `contents` method: 76 | The _index_ (string) is a key available in the content. 77 | The _order_ (boolean) is `true` for ascending and `false` for descending. 78 | 79 | To fetch every articles, indexed by _date_ and most recent first (descending): 80 | 81 | ``` php 82 | $app->get('/blog')->contents('article', 'date', false); 83 | ``` 84 | 85 | ### Pagination 86 | 87 | > You need your contents paginated? The method `paginate` works just like the method `contents` but paginate your contents. 88 | 89 | To tell Phpillip that your route is a _paginated list_ of contents, use the `contents` method on the route: 90 | 91 | ``` php 92 | // For an 'article' content type: 93 | $app->get('/blog')->paginate('article'); 94 | ``` 95 | 96 | The paginate helper adds a `page` optional parameter to the route so that it can handle the following urls: `/blog` and `/blog/{page}`. 97 | 98 | When building a route that paginates content, Phpillip will need to call the route for each page, depending on how much content you have: _blog_, _blog/2_, _blog/3_... 99 | 100 | __Note:__ the page parameters is ommitted on the first page. 101 | 102 | The _paginate_ helper then acts as a param converter. 103 | 104 | When a _Request_ hits a route that is declared as being a _paginated list_ −in our example `/blog/2`− Phpillip will count files in `src/Resources/data/article` and parse the subset corresponding to the requested page. 105 | 106 | The _paginate_ helper defines 3 attributes in the _Request_: 107 | 108 | Key | Type | Description 109 | ----------- | -------------------------- | ----------- 110 | `articles` | _array_ | The contents corresponding to the requested page. 111 | `page` | _integer_ | The requested page 112 | `paginator` | _Phpillip\Model\Paginator_ | The total number of pages in the pagination 113 | 114 | Request attributes are available in the controller as always: 115 | 116 | ``` php 117 | // As parameters: 118 | function (array $articles, Paginator $paginator, page) { 119 | // $articles contains only articles of the curent page 120 | } 121 | 122 | // And in the Request attributes: 123 | function (Request $request) { 124 | $articles = $request->attributes->get('articles'); 125 | $paginator = $request->attributes->get('paginator'); 126 | $page = $request->attributes->get('page'); 127 | } 128 | ``` 129 | 130 | ## Hide a route 131 | 132 | To hide a route from the build, use the `hide` method. 133 | 134 | ```php 135 | $app 136 | ->get('_latest-posts') 137 | ->content('post') 138 | ->bind('latest_posts') 139 | ->hide() 140 | ``` 141 | 142 | This can be useful for a route that is meant to be rendered as a part of other routes, but should not generate a static file on its own. 143 | 144 | Like a Twig render: `{{ render(url('latest_posts')) }}` 145 | 146 | ## Route format 147 | 148 | Route format is treated in its [dedicated documentation](../controller/format.md). 149 | 150 | ## Template 151 | 152 | Template resolution is treated in its [dedicated documentation](../controller/template.md). 153 | -------------------------------------------------------------------------------- /doc/feature/sitemap.md: -------------------------------------------------------------------------------- 1 | # Sitemap 2 | 3 | Phpillip automatically generates an XML sitemap of your website. 4 | The sitemap contains every URL called by the _build_ command. 5 | 6 | ## Set last modified 7 | 8 | When registering a url in the sitemap, the build command looks for a _Last-Modified_ header in the _Response_, if it exists it will be used as `` tag for this entry in the sitemap. 9 | 10 | So if you want to set a last modified tag for a custom controller, just set the _Last-Modified_ header in your response. 11 | 12 | __Tip:__ If your routes are [declared as _content_ routes](../feature/helpers.md), Phpillip automatically set that header based on last change of your content files. 13 | 14 | ## Hide a route from sitemap 15 | 16 | By default, all routes are registered in the sitemap. 17 | To hide a route from the sitemap, use the method `hideFromSitemap`; 18 | 19 | ``` php 20 | $app->get('blog/feed.rss')->hideFromSitemap(); 21 | ``` 22 | 23 | ## Disable Sitemap 24 | 25 | To disable the _sitemap_ feature completely, set it to `false` in the configuration: 26 | 27 | ``` yaml 28 | # src/Resources/config/config.yml 29 | sitemap: false 30 | ``` 31 | -------------------------------------------------------------------------------- /doc/feature/twig.md: -------------------------------------------------------------------------------- 1 | # Twig utils 2 | 3 | Phpillip register some Twig extensions to provide the following filters and functions: 4 | 5 | ## Functions 6 | 7 | __public:__ Get the relative or absolute url to a file in the public folder. 8 | 9 | Get the relative url to a stylesheet: 10 | 11 | ```twig 12 | 13 | ``` 14 | 15 | Get the absolute url to an image: 16 | 17 | ```twig 18 | 19 | ``` 20 | 21 | ## Filters 22 | 23 | __markdown:__ Parse a mardown string to HTML. 24 | 25 | ```twig 26 | {{ 'My *markdown* sentence'|markdown }} 27 | ``` 28 | -------------------------------------------------------------------------------- /doc/more/about.md: -------------------------------------------------------------------------------- 1 | # About Phpillip 2 | 3 | Hi, I'm [Thomas Jarrand](https://github.com/tom32i). 4 | I'm a web developer using Symfony a lot, I created and maintain Phpillip. 5 | 6 | I first built it as a tool to create my [technical blog](http://thomas.jarrand.fr/blog/). 7 | 8 | It's not meant to be the best PHP Website Generator around: its first goal was to fit my needs. 9 | 10 | However I documented and open-sourced it and will be happy if it helps any of you to build your projects. 11 | 12 | Open-source rocks, 13 | 14 | Thomas. 15 | -------------------------------------------------------------------------------- /doc/more/contribution.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you need any feature in Phpillip or encounter any bug: please [fill up an issue](https://github.com/Phpillip/phpillip/issues). 4 | 5 | __Whether you're a developer or not, any contribution is welcome:__ 6 | 7 | - Feedback 8 | - Code review 9 | - Test 10 | - Typo 11 | - Documentation 12 | - Idea / suggestion 13 | - Sharing / Blog post 14 | 15 | If you want to contribute to the source code, please feel free to submit a pull request in Github. 16 | 17 | Thank you guys! 18 | -------------------------------------------------------------------------------- /src/Application.php: -------------------------------------------------------------------------------- 1 | $this->getRoot()], 22 | $this->getConfiguration(), 23 | $values 24 | )); 25 | 26 | $this->registerServiceProviders(); 27 | } 28 | 29 | /** 30 | * Register service providers 31 | */ 32 | public function registerServiceProviders() 33 | { 34 | $this->register(new SilexProvider\HttpFragmentServiceProvider()); 35 | $this->register(new SilexProvider\UrlGeneratorServiceProvider()); 36 | $this->register(new SilexProvider\ServiceControllerServiceProvider()); 37 | $this->register(new PhpillipProvider\InformatorServiceProvider()); 38 | $this->register(new PhpillipProvider\PygmentsServiceProvider()); 39 | $this->register(new PhpillipProvider\ParsedownServiceProvider()); 40 | $this->register(new PhpillipProvider\DecoderServiceProvider()); 41 | $this->register(new PhpillipProvider\ContentServiceProvider()); 42 | $this->register(new PhpillipProvider\TwigServiceProvider()); 43 | $this->register(new PhpillipProvider\SubscriberServiceProvider()); 44 | $this->register(new PhpillipProvider\ContentControllerServiceProvider()); 45 | } 46 | 47 | /** 48 | * Load and return configuration 49 | * 50 | * @return array 51 | */ 52 | protected function getConfiguration() 53 | { 54 | $configurator = new Configurator($this, [$this->getRoot() . '/Resources/config']); 55 | 56 | return $configurator->getConfiguration(); 57 | } 58 | 59 | /** 60 | * Get root directory (the by Symfony Kernel's way) 61 | * 62 | * @return string 63 | */ 64 | protected function getRoot() 65 | { 66 | $reflection = new \ReflectionObject($this); 67 | 68 | return str_replace('\\', '/', dirname($reflection->getFileName())); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Behavior/PropertyHandlerInterface.php: -------------------------------------------------------------------------------- 1 | app = $app; 62 | $this->locator = new FileLocator($configDirectories); 63 | $this->resolver = new LoaderResolver([new YamlConfigLoader($this->locator)]); 64 | $this->loader = new DelegatingLoader($this->resolver); 65 | $this->processor = new Processor(); 66 | $this->configuration = new PhpillipConfiguration(); 67 | } 68 | 69 | /** 70 | * Get configuration 71 | * 72 | * @return array 73 | */ 74 | public function getConfiguration() 75 | { 76 | $configurationFiles = $this->locator->locate('config.yml', null, false); 77 | $configurations = []; 78 | 79 | foreach ($configurationFiles as $path) { 80 | $configurations[] = $this->loader->load($path); 81 | } 82 | 83 | return $this->processor->processConfiguration($this->configuration, $configurations); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Config/Definition/PhpillipConfiguration.php: -------------------------------------------------------------------------------- 1 | root('phpillip'); 20 | 21 | $rootNode 22 | ->children() 23 | ->scalarNode('default_controllers') 24 | ->info('Provides default controllers for contents') 25 | ->defaultFalse() 26 | ->end() 27 | ->scalarNode('route_class') 28 | ->info('Route class name') 29 | ->defaultValue('Phpillip\Routing\Route') 30 | ->end() 31 | ->scalarNode('parsedown_class') 32 | ->info('Parsedown service class name') 33 | ->defaultValue('Phpillip\Service\Parsedown') 34 | ->end() 35 | ->scalarNode('pygments_class') 36 | ->info('Pygments service class name') 37 | ->defaultValue('Phpillip\Service\Pygments') 38 | ->end() 39 | ->scalarNode('informator_class') 40 | ->info('Informator service class name') 41 | ->defaultValue('Phpillip\Service\Informator') 42 | ->end() 43 | ->scalarNode('content_repository_class') 44 | ->info('Content Repository service class name') 45 | ->defaultValue('Phpillip\Service\ContentRepository') 46 | ->end() 47 | ->scalarNode('src_path') 48 | ->defaultValue('/Resources/data') 49 | ->info('Content files directory') 50 | ->end() 51 | ->scalarNode('dst_path') 52 | ->defaultValue('/../dist') 53 | ->info('Build destination path') 54 | ->end() 55 | ->scalarNode('public_path') 56 | ->defaultValue('/Resources/public') 57 | ->info('Public files directory') 58 | ->end() 59 | ->scalarNode('twig_path') 60 | ->defaultValue('/Resources/views') 61 | ->info('Twig views directory') 62 | ->end() 63 | ->arrayNode('commands') 64 | ->info('A list of Command classnames to add to Phpillip console') 65 | ->prototype('scalar') 66 | ->cannotBeEmpty() 67 | ->validate() 68 | ->ifTrue(function ($value) { return !$this->isAValidCommand($value); }) 69 | ->thenInvalid('"%s" is not a valid Command.') 70 | ->end() 71 | ->end() 72 | ->end() 73 | ->scalarNode('sitemap') 74 | ->defaultTrue() 75 | ->info('Enable/Disable the XML sitemap generation') 76 | ->end() 77 | ->variableNode('parameters') 78 | ->info('Your key/value parameters.') 79 | ->end() 80 | ->end() 81 | ; 82 | 83 | return $treeBuilder; 84 | } 85 | 86 | /** 87 | * Is the given class name a valid Command? 88 | * 89 | * @param string $className 90 | * 91 | * @return boolean 92 | */ 93 | public function isAValidCommand($className) 94 | { 95 | return class_exists($className) && is_subclass_of($className, 'Symfony\Component\Console\Command\Command'); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Config/Loader/YamlConfigLoader.php: -------------------------------------------------------------------------------- 1 | import($resource)); 23 | } 24 | } 25 | 26 | return $config; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function supports($resource, $type = null) 33 | { 34 | return is_string($resource) && 'yml' === pathinfo($resource, PATHINFO_EXTENSION); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Console/Application.php: -------------------------------------------------------------------------------- 1 | kernel = $kernel; 30 | 31 | parent::__construct('Phpillip', $kernel::VERSION); 32 | 33 | $this->getDefinition()->addOption(new InputOption( 34 | '--no-debug', 35 | null, 36 | InputOption::VALUE_NONE, 37 | 'Switches off debug mode.' 38 | )); 39 | } 40 | 41 | /** 42 | * Gets the Kernel associated with this Console. 43 | * 44 | * @return KernelInterface A KernelInterface instance 45 | */ 46 | public function getKernel() 47 | { 48 | return $this->kernel; 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function doRun(InputInterface $input, OutputInterface $output) 55 | { 56 | $this->kernel->boot(); 57 | $this->kernel->flush(); 58 | 59 | return parent::doRun($input, $output); 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | protected function getDefaultCommands() 66 | { 67 | return array_merge( 68 | parent::getDefaultCommands(), 69 | [ 70 | new Command\BuildCommand(), 71 | new Command\ExposeCommand(), 72 | new Command\ServeCommand(), 73 | new Command\WatchCommand(), 74 | ], 75 | array_map(function ($className) { 76 | return new $className; 77 | }, $this->getKernel()['commands']) 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Console/Command/BuildCommand.php: -------------------------------------------------------------------------------- 1 | setName('phpillip:build') 59 | ->setDescription('Build static website') 60 | ->addArgument( 61 | 'host', 62 | InputArgument::OPTIONAL, 63 | 'What should be used as domain name for absolute url generation?' 64 | ) 65 | ->addArgument( 66 | 'destination', 67 | InputArgument::OPTIONAL, 68 | 'Full path to destination directory' 69 | ) 70 | ->addOption( 71 | 'no-sitemap', 72 | null, 73 | InputOption::VALUE_NONE, 74 | 'Don\'t build the sitemap' 75 | ) 76 | ->addOption( 77 | 'no-expose', 78 | null, 79 | InputOption::VALUE_NONE, 80 | 'Don\'t expose the public directory after build' 81 | ) 82 | ; 83 | } 84 | 85 | /** 86 | * {@inheritdoc} 87 | */ 88 | protected function initialize(InputInterface $input, OutputInterface $output) 89 | { 90 | $this->app = $this->getApplication()->getKernel(); 91 | $this->logger = new Logger($output); 92 | 93 | $destination = $input->getArgument('destination') ?: $this->app['root'] . $this->app['dst_path']; 94 | 95 | $this->builder = new Builder($this->app, $destination); 96 | 97 | if (!$input->getOption('no-sitemap')) { 98 | $this->sitemap = new Sitemap(); 99 | $this->app['dispatcher']->addSubscriber(new SitemapListener($this->app['routes'], $this->sitemap)); 100 | } 101 | 102 | if ($host = $input->getArgument('host')) { 103 | $this->app['url_generator']->getContext()->setHost($host); 104 | } 105 | } 106 | 107 | /** 108 | * {@inheritdoc} 109 | */ 110 | protected function execute(InputInterface $input, OutputInterface $output) 111 | { 112 | $this->logger->log('[ Clearing destination folder ]'); 113 | 114 | $this->builder->clear(); 115 | 116 | $this->logger->log(sprintf('[ Building %s routes ]', $this->app['routes']->count())); 117 | 118 | foreach ($this->app['routes'] as $name => $route) { 119 | $this->dump($route, $name); 120 | } 121 | 122 | if ($this->sitemap) { 123 | $this->logger->log(sprintf('[ Building sitemap with %s urls. ]', count($this->sitemap))); 124 | $this->buildSitemap($this->sitemap); 125 | } 126 | 127 | if (!$input->getOption('no-expose') && $this->getApplication()->has('phpillip:expose')) { 128 | $arguments = [ 129 | 'command' => 'phpillip:expose', 130 | 'destination' => $input->getArgument('destination'), 131 | ]; 132 | $this->getApplication()->get('phpillip:expose')->run(new ArrayInput($arguments), $output); 133 | } 134 | } 135 | 136 | /** 137 | * Dump route content to destination file 138 | * 139 | * @param Route $route 140 | * @param string $name 141 | */ 142 | protected function dump(Route $route, $name) 143 | { 144 | if (!$route->isVisible()) { 145 | return; 146 | } 147 | 148 | if (!in_array('GET', $route->getMethods())) { 149 | throw new Exception(sprintf('Only GET mehtod supported, "%s" given.', $name)); 150 | } 151 | 152 | if ($route->hasContent()) { 153 | if ($route->isList()) { 154 | if ($route->isPaginated()) { 155 | $this->buildPaginatedRoute($route, $name); 156 | } else { 157 | $this->buildListRoute($route, $name); 158 | } 159 | } else { 160 | $this->buildContentRoute($route, $name); 161 | } 162 | } else { 163 | $this->logger->log(sprintf('Building route %s', $name)); 164 | $this->builder->build($route, $name); 165 | } 166 | } 167 | 168 | /** 169 | * Build paginated route 170 | * 171 | * @param Route $route 172 | * @param string $name 173 | */ 174 | protected function buildPaginatedRoute(Route $route, $name) 175 | { 176 | $contentType = $route->getContent(); 177 | $contents = $this->app['content_repository']->listContents($contentType); 178 | $paginator = new Paginator($contents, $route->getPerPage()); 179 | 180 | $this->logger->log(sprintf( 181 | 'Building route %s for %s pages', 182 | $name, 183 | $paginator->count() 184 | )); 185 | $this->logger->getProgress($paginator->count()); 186 | $this->logger->start(); 187 | 188 | foreach ($paginator as $index => $contents) { 189 | $this->builder->build($route, $name, ['page' => $index + 1]); 190 | $this->logger->advance(); 191 | } 192 | 193 | $this->logger->finish(); 194 | } 195 | 196 | /** 197 | * Build list route 198 | * 199 | * @param Route $route 200 | * @param string $name 201 | */ 202 | protected function buildListRoute(Route $route, $name) 203 | { 204 | $contentType = $route->getContent(); 205 | $contents = $this->app['content_repository']->listContents($contentType); 206 | 207 | $this->logger->log(sprintf( 208 | 'Building route %s with %s %s(s)', 209 | $name, 210 | count($contents), 211 | $contentType 212 | )); 213 | $this->builder->build($route, $name); 214 | } 215 | 216 | /** 217 | * Build content route 218 | * 219 | * @param Route $route 220 | * @param string $name 221 | */ 222 | protected function buildContentRoute(Route $route, $name) 223 | { 224 | $contentType = $route->getContent(); 225 | $contents = $this->app['content_repository']->listContents($contentType); 226 | 227 | $this->logger->log(sprintf( 228 | 'Building route %s for %s %s(s)', 229 | $name, 230 | count($contents), 231 | $route->getContent() 232 | )); 233 | $this->logger->getProgress(count($contents)); 234 | $this->logger->start(); 235 | 236 | foreach ($contents as $content) { 237 | $this->builder->build($route, $name, [$contentType => $content]); 238 | $this->logger->advance(); 239 | } 240 | 241 | $this->logger->finish(); 242 | } 243 | 244 | /** 245 | * Build sitemap xml file from Sitemap 246 | * 247 | * @param Sitemap $sitemap 248 | */ 249 | protected function buildSitemap(Sitemap $sitemap) 250 | { 251 | $content = $this->app['twig']->render('@phpillip/sitemap.xml.twig', ['sitemap' => $sitemap]); 252 | $this->builder->write('/', $content, 'xml', 'sitemap'); 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/Console/Command/ExposeCommand.php: -------------------------------------------------------------------------------- 1 | setName('phpillip:expose') 24 | ->setDescription('Expose the public directory') 25 | ->addArgument('destination', InputArgument::OPTIONAL, 'Full path to destination directory') 26 | ; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | protected function execute(InputInterface $input, OutputInterface $output) 33 | { 34 | $output->writeln('[ Exposing public directory ]'); 35 | 36 | $app = $this->getApplication()->getKernel(); 37 | $source = $app['root'] . $app['public_path']; 38 | $destination = $input->getArgument('destination') ?: $app['root'] . $app['dst_path']; 39 | $finder = new Finder(); 40 | $files = new Filesystem(); 41 | 42 | foreach ($finder->files()->in($source) as $file) { 43 | $files->copy( 44 | $file->getPathName(), 45 | str_replace($source, $destination, $file->getPathName()), 46 | true 47 | ); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Console/Command/ServeCommand.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class ServeCommand extends Command 22 | { 23 | /** 24 | * Application 25 | * 26 | * @var Phpillip\Application 27 | */ 28 | protected $app; 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | protected function initialize(InputInterface $input, OutputInterface $output) 34 | { 35 | $this->app = $this->getApplication()->getKernel(); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function configure() 42 | { 43 | $this 44 | ->setName('phpillip:serve') 45 | ->setDescription('Runs your Phpillip application with PHP built-in web server') 46 | ->setDefinition([ 47 | new InputArgument('address', InputArgument::OPTIONAL, 'Address:port', '127.0.0.1'), 48 | new InputOption('port', 'p', InputOption::VALUE_REQUIRED, 'Address port number', '8000'), 49 | ]) 50 | ->setHelp(<<%command.name% runs PHP built-in web server: 52 | 53 | %command.full_name% 54 | 55 | To change default bind address and port use the address argument: 56 | 57 | %command.full_name% 127.0.0.1:8080 58 | 59 | See also: http://www.php.net/manual/en/features.commandline.webserver.php 60 | 61 | EOF 62 | ) 63 | ; 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | protected function execute(InputInterface $input, OutputInterface $output) 70 | { 71 | $documentRoot = $this->app['root'] . $this->app['public_path']; 72 | 73 | if (!is_dir($documentRoot)) { 74 | $output->writeln(sprintf('The document root directory "%s" does not exist', $documentRoot)); 75 | 76 | return 1; 77 | } 78 | 79 | $address = $input->getArgument('address'); 80 | 81 | if (false === strpos($address, ':')) { 82 | $address = $address.':'.$input->getOption('port'); 83 | } 84 | 85 | if ($this->isOtherServerProcessRunning($address)) { 86 | $output->writeln(sprintf('A process is already listening on http://%s.', $address)); 87 | 88 | return 1; 89 | } 90 | 91 | $output->writeln(sprintf("Server running on http://%s\n", $address)); 92 | $output->writeln('Quit the server with CONTROL-C.'); 93 | 94 | if (null === $builder = $this->createPhpProcessBuilder($output, $address)) { 95 | return 1; 96 | } 97 | 98 | $builder->setWorkingDirectory($documentRoot); 99 | $builder->setTimeout(null); 100 | $process = $builder->getProcess(); 101 | 102 | if (OutputInterface::VERBOSITY_VERBOSE > $output->getVerbosity()) { 103 | $process->disableOutput(); 104 | } 105 | 106 | $this 107 | ->getHelper('process') 108 | ->run($output, $process, null, null, OutputInterface::VERBOSITY_VERBOSE); 109 | 110 | if (!$process->isSuccessful()) { 111 | $output->writeln('Built-in server terminated unexpectedly'); 112 | 113 | if ($process->isOutputDisabled()) { 114 | $output->writeln('Run the command again with -v option for more details'); 115 | } 116 | } 117 | 118 | return $process->getExitCode(); 119 | } 120 | 121 | /** 122 | * Get a ProcessBuilder instance 123 | * 124 | * @param OutputInterface $output 125 | * @param string $address 126 | * 127 | * @return ProcessBuilder 128 | */ 129 | private function createPhpProcessBuilder(OutputInterface $output, $address) 130 | { 131 | $router = realpath(__DIR__ . '/../../Resources/bin/router.php'); 132 | $finder = new PhpExecutableFinder(); 133 | 134 | if (false === $binary = $finder->find()) { 135 | $output->writeln('Unable to find PHP binary to run server'); 136 | 137 | return; 138 | } 139 | 140 | return new ProcessBuilder([$binary, '-S', $address, $router]); 141 | } 142 | 143 | /** 144 | * Is another service already using the given address? 145 | * 146 | * @param string $address 147 | * 148 | * @return boolean 149 | */ 150 | protected function isOtherServerProcessRunning($address) 151 | { 152 | if (file_exists(sys_get_temp_dir().'/'.strtr($address, '.:', '--').'.pid')) { 153 | return true; 154 | } 155 | 156 | list($hostname, $port) = explode(':', $address); 157 | 158 | if (false !== $fp = @fsockopen($hostname, $port, $errno, $errstr, 5)) { 159 | fclose($fp); 160 | 161 | return true; 162 | } 163 | 164 | return false; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Console/Command/WatchCommand.php: -------------------------------------------------------------------------------- 1 | setName('phpillip:watch') 46 | ->setDescription('Watch for changes in the sources to re-run the build') 47 | ->addOption('period', null, InputOption::VALUE_REQUIRED, 'Set the polling period in seconds', 1) 48 | ; 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | protected function initialize(InputInterface $input, OutputInterface $output) 55 | { 56 | $this->command = $this->getApplication()->get('phpillip:build'); 57 | $this->input = new ArrayInput(['command' => $this->command->getName()]); 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | protected function execute(InputInterface $input, OutputInterface $output) 64 | { 65 | $period = $input->getOption('period'); 66 | $source = $this->getApplication()->getKernel()['root']; 67 | 68 | $this->build($output); 69 | 70 | $output->writeln(sprintf('[ Watching for changes in %s ]', $source)); 71 | 72 | while (true) { 73 | $finder = new Finder(); 74 | 75 | if ($finder->in($source)->date(sprintf('since %s', $this->lastBuild->format('c')))->count()) { 76 | $this->build($output); 77 | } 78 | 79 | sleep($period); 80 | } 81 | } 82 | 83 | /** 84 | * Run the build command 85 | * 86 | * @param OutputInterface $output 87 | */ 88 | protected function build(OutputInterface $output) 89 | { 90 | $this->command->run($this->input, $output); 91 | 92 | $this->lastBuild = new DateTime(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Console/EventListener/SitemapListener.php: -------------------------------------------------------------------------------- 1 | routes = $routes; 40 | $this->sitemap = $sitemap; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public static function getSubscribedEvents() 47 | { 48 | return [ 49 | KernelEvents::RESPONSE => 'onKernelReponse', 50 | ]; 51 | } 52 | 53 | /** 54 | * Handler Kernel Response events 55 | * 56 | * @param FilterResponseEvent $event 57 | */ 58 | public function onKernelReponse(FilterResponseEvent $event) 59 | { 60 | $request = $event->getRequest(); 61 | $response = $event->getResponse(); 62 | $route = $this->routes->get($request->attributes->get('_route')); 63 | 64 | if ($route && $route->isMapped()) { 65 | $url = $request->attributes->get('_canonical'); 66 | $lastModified = new DateTime($response->headers->get('Last-Modified')); 67 | $this->sitemap->add($url, $lastModified); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Console/Model/Builder.php: -------------------------------------------------------------------------------- 1 | app = $app; 46 | $this->destination = $destination; 47 | $this->files = new Filesystem(); 48 | } 49 | 50 | /** 51 | * Clear destination folder 52 | */ 53 | public function clear() 54 | { 55 | if ($this->files->exists($this->destination)) { 56 | $this->files->remove($this->destination); 57 | } 58 | 59 | $this->files->mkdir($this->destination); 60 | } 61 | 62 | /** 63 | * Dump the given Route into a file 64 | * 65 | * @param Route $route 66 | * @param string $name 67 | * @param array $parameters 68 | */ 69 | public function build(Route $route, $name, array $parameters = []) 70 | { 71 | $url = $this->app['url_generator']->generate($name, $parameters, UrlGeneratorInterface::ABSOLUTE_URL); 72 | $request = Request::create($url, 'GET', array_merge(['_format' => $route->getFormat()], $parameters)); 73 | $response = $this->app->handle($request); 74 | 75 | $this->write( 76 | $this->getFilePath($route, $parameters), 77 | $response->getContent(), 78 | $request->getFormat($response->headers->get('Content-Type')), 79 | $route->getFileName() 80 | ); 81 | } 82 | 83 | /** 84 | * Write a file 85 | * 86 | * @param string $path The directory to put the file in (in the current destination) 87 | * @param string $content The file content 88 | * @param string $filename The file name 89 | * @param string $extension The file extension 90 | */ 91 | public function write($path, $content, $extension = 'html', $filename = 'index') 92 | { 93 | $directory = sprintf('%s/%s', $this->destination, trim($path, '/')); 94 | $file = sprintf('%s.%s', $filename, $extension); 95 | 96 | if (!$this->files->exists($directory)) { 97 | $this->files->mkdir($directory); 98 | } 99 | 100 | $this->files->dumpFile(sprintf('%s/%s', $directory, $file), $content); 101 | } 102 | 103 | /** 104 | * Get destination file path for the given route / parameters 105 | * 106 | * @param Route $route 107 | * @param array $parameters 108 | * 109 | * @return string 110 | */ 111 | protected function getFilePath(Route $route, array $parameters = []) 112 | { 113 | $filepath = trim($route->getFilePath(), '/'); 114 | 115 | foreach ($route->getDefaults() as $key => $value) { 116 | if (isset($parameters[$key]) && $parameters[$key] == $value) { 117 | $filepath = rtrim(preg_replace(sprintf('#{%s}/?#', $key), null, $filepath), '/'); 118 | } 119 | } 120 | 121 | foreach ($parameters as $key => $value) { 122 | $filepath = str_replace(sprintf('{%s}', $key), (string) $value, $filepath); 123 | } 124 | 125 | return $filepath; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Console/Model/Logger.php: -------------------------------------------------------------------------------- 1 | output = $output; 42 | $this->logs = []; 43 | } 44 | 45 | /** 46 | * Log 47 | * 48 | * @param string $message 49 | */ 50 | public function log($message) 51 | { 52 | if ($this->progress) { 53 | $this->progress->setMessage($message); 54 | $this->logs[] = $message; 55 | } else { 56 | $this->output->writeLn($message); 57 | } 58 | } 59 | 60 | /** 61 | * Get progress bar instance 62 | * 63 | * @param integer $total 64 | * 65 | * @return ProgressBar 66 | */ 67 | public function getProgress($total = null) 68 | { 69 | if (!$this->progress || $this->progress->getMaxSteps() === $this->progress->getProgress()) { 70 | $this->progress = new ProgressBar($this->output, $total); 71 | } 72 | 73 | return $this->progress; 74 | } 75 | 76 | /** 77 | * Start progress 78 | */ 79 | public function start() 80 | { 81 | if ($this->progress) { 82 | $this->progress->start(); 83 | } 84 | } 85 | 86 | /** 87 | * Advance progress 88 | */ 89 | public function advance() 90 | { 91 | if ($this->progress) { 92 | $this->progress->advance(); 93 | } 94 | } 95 | 96 | /** 97 | * Finish progress 98 | */ 99 | public function finish() 100 | { 101 | if ($this->progress) { 102 | $this->progress->finish(); 103 | $this->progress = null; 104 | $this->flush(); 105 | } 106 | } 107 | 108 | /** 109 | * Flush message queue 110 | */ 111 | public function flush() 112 | { 113 | if (!$this->progress) { 114 | $this->log(''); 115 | 116 | foreach ($this->logs as $message) { 117 | $this->log($message); 118 | } 119 | 120 | $this->logs = []; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Console/Model/Sitemap.php: -------------------------------------------------------------------------------- 1 | $location]; 39 | 40 | if ($priority === null && empty($this->urls)) { 41 | $priority = 0; 42 | } 43 | 44 | if ($lastModified) { 45 | $url['lastModified'] = $lastModified; 46 | } 47 | 48 | if ($priority !== null) { 49 | $url['priority'] = $priority; 50 | } 51 | 52 | if ($frequency) { 53 | $url['frequency'] = $frequency; 54 | } 55 | 56 | $this->urls[$location] = $url; 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function rewind() 63 | { 64 | $this->position = 0; 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function current() 71 | { 72 | return $this->urls[array_keys($this->urls)[$this->position]]; 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | public function key() 79 | { 80 | return array_keys($this->urls)[$this->position]; 81 | } 82 | 83 | /** 84 | * {@inheritdoc} 85 | */ 86 | public function next() 87 | { 88 | ++$this->position; 89 | } 90 | 91 | /** 92 | * {@inheritdoc} 93 | */ 94 | public function valid() 95 | { 96 | return isset(array_keys($this->urls)[$this->position]); 97 | } 98 | 99 | /** 100 | * {@inheritdoc} 101 | */ 102 | public function count() 103 | { 104 | return count($this->urls); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Controller/ContentController.php: -------------------------------------------------------------------------------- 1 | extractViewParameters($request, ['app']); 25 | } 26 | 27 | /** 28 | * Paginated list of contents 29 | * 30 | * @param Request $request 31 | * @param Application $app 32 | * @param Paginator $paginator 33 | * 34 | * @return array 35 | */ 36 | public function page(Request $request, Application $app, Paginator $paginator) 37 | { 38 | return array_merge( 39 | ['pages' => count($paginator)], 40 | $this->extractViewParameters($request, ['app', 'paginator']) 41 | ); 42 | } 43 | 44 | /** 45 | * Show a single content 46 | * 47 | * @param Request $request 48 | * @param Application $app 49 | * 50 | * @return array 51 | */ 52 | public function show(Request $request, Application $app) 53 | { 54 | return $this->extractViewParameters($request, ['app']); 55 | } 56 | 57 | /** 58 | * Extract view parameters from Request attributes 59 | * 60 | * @param Request $request 61 | * @param array $exclude Keys to exclude from view 62 | * 63 | * @return array 64 | */ 65 | protected function extractViewParameters(Request $request, array $exclude = []) 66 | { 67 | $parameters = []; 68 | 69 | foreach ($request->attributes as $key => $value) { 70 | if (strpos($key, '_') !== 0 && !in_array($key, $exclude)) { 71 | $parameters[$key] = $value; 72 | } 73 | } 74 | 75 | return $parameters; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Encoder/MarkdownDecoder.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function decode($data, $format, array $context = array()) 45 | { 46 | $separator = static::HEAD_SEPARATOR; 47 | $start = strpos($data, $separator); 48 | $stop = strpos($data, $separator, 1); 49 | $length = strlen($separator) + 1; 50 | 51 | if ($start === 0 && $stop) { 52 | return array_merge( 53 | $this->parseYaml(substr($data, $start + $length, $stop - $length)), 54 | ['content' => $this->markdownify(substr($data, $stop + $length))] 55 | ); 56 | } 57 | 58 | return ['content' => $this->markdownify($data)]; 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function supportsDecoding($format) 65 | { 66 | return self::FORMAT === $format; 67 | } 68 | 69 | /** 70 | * Parse YAML 71 | * 72 | * @param string $data 73 | * 74 | * @return array 75 | */ 76 | protected function parseYaml($data) 77 | { 78 | return Yaml::parse($data, true); 79 | } 80 | 81 | /** 82 | * Parse Mardown to return HTML 83 | * 84 | * @param string $data 85 | * 86 | * @return string 87 | */ 88 | protected function markdownify($data) 89 | { 90 | return $this->parser->parse($data); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Encoder/YamlEncoder.php: -------------------------------------------------------------------------------- 1 | routes = $routes; 42 | $this->repository = $repository; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public static function getSubscribedEvents() 49 | { 50 | return [ 51 | KernelEvents::CONTROLLER => 'onKernelController', 52 | ]; 53 | } 54 | 55 | /** 56 | * Handler Kernel Controller events 57 | * 58 | * @param FilterControllerEvent $event 59 | */ 60 | public function onKernelController(FilterControllerEvent $event) 61 | { 62 | $request = $event->getRequest(); 63 | $route = $this->routes->get($request->attributes->get('_route')); 64 | 65 | if ($route && $route->hasContent()) { 66 | $this->populateContent($route, $request); 67 | } 68 | } 69 | 70 | /** 71 | * Populate content for the request 72 | * 73 | * @param Route $route 74 | * @param Request $request 75 | */ 76 | protected function populateContent(Route $route, Request $request) 77 | { 78 | $content = $route->getContent(); 79 | $name = $content; 80 | 81 | if ($route->isList()) { 82 | $name .= 's'; 83 | $value = $this->repository->getContents($content, $route->getIndexBy(), $route->getOrder()); 84 | 85 | if ($route->isPaginated()) { 86 | $paginator = new Paginator($value, $route->getPerPage()); 87 | $value = $paginator->get($request->attributes->get('page')); 88 | $request->attributes->set('paginator', $paginator); 89 | } 90 | } else { 91 | $value = $this->repository->getContent($content, $request->attributes->get($content)); 92 | } 93 | 94 | $request->attributes->set($name, $value); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/EventListener/LastModifiedListener.php: -------------------------------------------------------------------------------- 1 | routes = $routes; 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public static function getSubscribedEvents() 40 | { 41 | return [ 42 | KernelEvents::RESPONSE => 'onKernelReponse', 43 | ]; 44 | } 45 | 46 | /** 47 | * Handler Kernel Response events 48 | * 49 | * @param FilterResponseEvent $event 50 | */ 51 | public function onKernelReponse(FilterResponseEvent $event) 52 | { 53 | $request = $event->getRequest(); 54 | $response = $event->getResponse(); 55 | $route = $this->routes->get($request->attributes->get('_route')); 56 | 57 | if ($route && $route->hasContent() && !$response->headers->has('Last-Modified')) { 58 | $this->setLastModifiedHeader($route, $request, $response); 59 | } 60 | } 61 | 62 | /** 63 | * Populate content for the request 64 | * 65 | * @param Route $route 66 | * @param Request $request 67 | * @param Response $response 68 | */ 69 | protected function setLastModifiedHeader(Route $route, Request $request, Response $response) 70 | { 71 | $content = $route->getContent(); 72 | 73 | if ($route->isList()) { 74 | $dates = array_map(function (array $content) { 75 | return $content['lastModified']; 76 | }, $request->attributes->get($content . 's')); 77 | 78 | rsort($dates); 79 | 80 | $lastModified = $dates[0]; 81 | } else { 82 | $lastModified = $request->attributes->get($content)['lastModified']; 83 | } 84 | 85 | $response->headers->set('Last-Modified', $lastModified); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/EventListener/TemplateListener.php: -------------------------------------------------------------------------------- 1 | routes = $routes; 36 | $this->templating = $templating; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public static function getSubscribedEvents() 43 | { 44 | return [ 45 | KernelEvents::CONTROLLER => 'onKernelController', 46 | KernelEvents::VIEW => 'onKernelView', 47 | ]; 48 | } 49 | 50 | /** 51 | * Handles Kernel Controller events 52 | * 53 | * @param FilterControllerEvent $event 54 | */ 55 | public function onKernelController(FilterControllerEvent $event) 56 | { 57 | $request = $event->getRequest(); 58 | 59 | if ($template = $this->getTemplate($request, $event->getController())) { 60 | $request->attributes->set('_template', $template); 61 | } 62 | } 63 | 64 | /** 65 | * Handles Kernel View events 66 | * 67 | * @param GetResponseForControllerResultEvent $event 68 | */ 69 | public function onKernelView(GetResponseForControllerResultEvent $event) 70 | { 71 | $request = $event->getRequest(); 72 | $response = $event->getResponse(); 73 | $parameters = $event->getControllerResult(); 74 | 75 | if (!$response instanceof Response && $template = $request->attributes->get('_template')) { 76 | return $event->setControllerResult($this->templating->render($template, $parameters)); 77 | } 78 | } 79 | 80 | /** 81 | * Get template from the given request and controller 82 | * 83 | * @param Request $request 84 | * @param mixed $controller 85 | * 86 | * @return string|null 87 | */ 88 | protected function getTemplate(Request $request, $controller) 89 | { 90 | if ($request->attributes->has('_template')) { 91 | return null; 92 | } 93 | 94 | $format = $request->attributes->get('_format', 'html'); 95 | $templates = []; 96 | 97 | if ($controllerInfo = $this->parseController($controller)) { 98 | $template = sprintf('%s/%s.%s.twig', $controllerInfo['name'], $controllerInfo['action'], $format); 99 | 100 | if ($this->templateExists($template)) { 101 | return $template; 102 | } else { 103 | $templates[] = $template; 104 | } 105 | } 106 | 107 | $route = $this->routes->get($request->attributes->get('_route')); 108 | 109 | if ($route && $route->hasContent()) { 110 | $template = sprintf('%s/%s.%s.twig', $route->getContent(), $route->isList() ? 'list' : 'show', $format); 111 | 112 | if ($this->templateExists($template)) { 113 | return $template; 114 | } else { 115 | $templates[] = $template; 116 | } 117 | } 118 | 119 | return array_pop($templates); 120 | } 121 | 122 | /** 123 | * Parse controller to extract its name 124 | * 125 | * @param mixed $controller 126 | * 127 | * @return string 128 | */ 129 | protected function parseController($controller) 130 | { 131 | if (!is_array($controller) || !is_object($controller[0]) || !isset($controller[1])) { 132 | return null; 133 | } 134 | 135 | if (!preg_match('#Controller\\\(.+)Controller$#', get_class($controller[0]), $matches)) { 136 | return null; 137 | } 138 | 139 | return ['name' => $matches[1], 'action' => $controller[1]]; 140 | } 141 | 142 | /** 143 | * Does the given template exists? 144 | * 145 | * @param string $template 146 | * 147 | * @return boolean 148 | */ 149 | protected function templateExists($template) 150 | { 151 | try { 152 | $class = $this->templating->getTemplateClass($template); 153 | } catch (Twig_Error_Loader $e) { 154 | return false; 155 | } 156 | 157 | return $template; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Model/Paginator.php: -------------------------------------------------------------------------------- 1 | pages = array_chunk($contents, $perPage); 37 | } 38 | 39 | /** 40 | * Get contents for the given page 41 | * 42 | * @param integer $page 43 | * 44 | * @return array 45 | */ 46 | public function get($page = 1) 47 | { 48 | $index = $page - 1; 49 | 50 | if (!isset($this->pages[$index])) { 51 | throw new RuntimeException(sprintf('Invalid page %s of %s', $page, $this->count())); 52 | } 53 | 54 | return $this->pages[$index]; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function rewind() 61 | { 62 | $this->position = 0; 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | public function current() 69 | { 70 | return $this->pages[$this->position]; 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function key() 77 | { 78 | return $this->position; 79 | } 80 | 81 | /** 82 | * {@inheritdoc} 83 | */ 84 | public function next() 85 | { 86 | ++$this->position; 87 | } 88 | 89 | /** 90 | * {@inheritdoc} 91 | */ 92 | public function valid() 93 | { 94 | return isset($this->pages[$this->position]); 95 | } 96 | 97 | /** 98 | * Get number of pages fo the given contents 99 | * 100 | * @return integer 101 | */ 102 | public function count() 103 | { 104 | return count($this->pages); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/PropertyHandler/CallbackPropertyHandler.php: -------------------------------------------------------------------------------- 1 | property = $property; 35 | $this->callback = $callback; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function getProperty() 42 | { 43 | return $this->property; 44 | } 45 | 46 | /** 47 | * Is data supported? 48 | * 49 | * @param array $data 50 | * 51 | * @return boolean 52 | */ 53 | public function isSupported(array $data) 54 | { 55 | return isset($data[$this->getProperty()]); 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function handle($value, array $context) 62 | { 63 | return call_user_func($this->callback, $value, $context); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/PropertyHandler/DateTimePropertyHandler.php: -------------------------------------------------------------------------------- 1 | property = $property; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function getProperty() 35 | { 36 | return $this->property; 37 | } 38 | 39 | /** 40 | * Is data supported? 41 | * 42 | * @param array $data 43 | * 44 | * @return boolean 45 | */ 46 | public function isSupported(array $data) 47 | { 48 | try { 49 | new DateTime($data[$this->getProperty()]); 50 | } catch (Exception $e) { 51 | return false; 52 | } 53 | 54 | return true; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function handle($value, array $context) 61 | { 62 | return new DateTime($value); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/PropertyHandler/IntegerPropertyHandler.php: -------------------------------------------------------------------------------- 1 | property = $property; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function getProperty() 33 | { 34 | return $this->property; 35 | } 36 | 37 | /** 38 | * Is data supported? 39 | * 40 | * @param array $data 41 | * 42 | * @return boolean 43 | */ 44 | public function isSupported(array $data) 45 | { 46 | return isset($data[$this->getProperty()]); 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function handle($value, array $context) 53 | { 54 | return intval($value); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/PropertyHandler/LastModifiedPropertyHandler.php: -------------------------------------------------------------------------------- 1 | getProperty()]); 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function handle($value, array $context) 33 | { 34 | $lastModified = new DateTime(); 35 | $lastModified->setTimestamp($context['file']->getMTime()); 36 | 37 | return $lastModified; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/PropertyHandler/SlugPropertyHandler.php: -------------------------------------------------------------------------------- 1 | getProperty()]); 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function handle($value, array $context) 33 | { 34 | return ContentRepository::getName($context['file']); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Provider/AutoControllerProvider.php: -------------------------------------------------------------------------------- 1 | directories()->in($source) as $file) { 24 | $content = $file->getFilename(); 25 | 26 | $controllers 27 | ->get(sprintf('/%s', $content), 'content.controller:index') 28 | ->contents($content) 29 | ->bind(sprintf('%s_list', $content)); 30 | 31 | $controllers 32 | ->get(sprintf('/%s/{%s}', $content, $content), 'content.controller:show') 33 | ->content($content) 34 | ->bind(sprintf('%s_show', $content)); 35 | } 36 | 37 | return $controllers; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Provider/ContentControllerServiceProvider.php: -------------------------------------------------------------------------------- 1 | share(function () { 20 | return new ContentController(); 21 | }); 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function boot(Application $app) 28 | { 29 | if ($app['default_controllers']) { 30 | $app->mount('', new AutoControllerProvider()); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Provider/ContentServiceProvider.php: -------------------------------------------------------------------------------- 1 | share(function ($app) { 21 | return new $app['content_repository_class']($app['decoder'], $app['root'] . $app['src_path']); 22 | }); 23 | 24 | $app['content_repository'] 25 | ->addPropertyHandler(new PropertyHandler\DateTimePropertyHandler()) 26 | ->addPropertyHandler(new PropertyHandler\IntegerPropertyHandler('weight')) 27 | ->addPropertyHandler(new PropertyHandler\LastModifiedPropertyHandler()) 28 | ->addPropertyHandler(new PropertyHandler\SlugPropertyHandler()); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function boot(Application $app) 35 | { 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Provider/DecoderServiceProvider.php: -------------------------------------------------------------------------------- 1 | share(function ($app) { 22 | return new Encoder\XmlEncoder(); 23 | }); 24 | 25 | $app['decoder.json'] = $app->share(function ($app) { 26 | return new Encoder\JsonEncoder(); 27 | }); 28 | 29 | $app['decoder.markdown'] = $app->share(function ($app) { 30 | return new PhpillipEncoder\MarkdownDecoder($app['parsedown']); 31 | }); 32 | 33 | $app['decoder.yaml'] = $app->share(function ($app) { 34 | return new PhpillipEncoder\YamlEncoder(); 35 | }); 36 | 37 | $app['content_decoders'] = [ 38 | $app['decoder.xml'], 39 | $app['decoder.json'], 40 | $app['decoder.yaml'], 41 | $app['decoder.markdown'], 42 | ]; 43 | 44 | $app['decoder'] = $app->share(function ($app) { 45 | return new Serializer([], $app['content_decoders']); 46 | }); 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function boot(Application $app) 53 | { 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Provider/HostServiceProvider.php: -------------------------------------------------------------------------------- 1 | getContext()->setHost($app['host']); 20 | } 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function boot(Application $app) 27 | { 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Provider/InformatorServiceProvider.php: -------------------------------------------------------------------------------- 1 | share(function ($app) { 20 | return new $app['informator_class']($app['url_generator']); 21 | }); 22 | 23 | $app->before([$app['informator'], 'beforeRequest']); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function boot(Application $app) 30 | { 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Provider/ParsedownServiceProvider.php: -------------------------------------------------------------------------------- 1 | share(function ($app) { 19 | return new $app['parsedown_class'](isset($app['pygments']) ? $app['pygments'] : null); 20 | }); 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function boot(Application $app) 27 | { 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Provider/PygmentsServiceProvider.php: -------------------------------------------------------------------------------- 1 | share(function ($app) { 20 | return new $app['pygments_class'](); 21 | }); 22 | } 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function boot(Application $app) 29 | { 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Provider/SubscriberServiceProvider.php: -------------------------------------------------------------------------------- 1 | addSubscriber(new ContentConverterListener($app['routes'], $app['content_repository'])); 22 | $app['dispatcher']->addSubscriber(new LastModifiedListener($app['routes'])); 23 | $app['dispatcher']->addSubscriber(new TemplateListener($app['routes'], $app['twig'])); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function boot(Application $app) 30 | { 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Provider/TwigServiceProvider.php: -------------------------------------------------------------------------------- 1 | addPath(__DIR__ . '/../Resources/views', 'phpillip'); 27 | 28 | // Add parameters to Twig globals 29 | $app['twig']->addGlobal('parameters', $app['parameters']); 30 | 31 | // Set up Public Extension as a service 32 | $app['twig_extension.public'] = $app->share(function ($app) { 33 | return new PublicExtension(); 34 | }); 35 | $app->before([$app['twig_extension.public'], 'beforeRequest']); 36 | 37 | // Register extensions 38 | $app['twig']->addExtension(new MarkdownExtension($app['parsedown'])); 39 | $app['twig']->addExtension($app['twig_extension.public']); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Resources/bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | hasParameterOption(array('--no-debug', '')); 26 | 27 | if ($debug) { 28 | Debug::enable(); 29 | } 30 | 31 | $application = new Console(new Application(['debug' => $debug])); 32 | $application->run($input); 33 | -------------------------------------------------------------------------------- /src/Resources/bin/router.php: -------------------------------------------------------------------------------- 1 | true]); 19 | $application->run(); 20 | -------------------------------------------------------------------------------- /src/Resources/views/rss.xml.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | {{ description }} 6 | {{ language|default('en') }} 7 | {{ link|default(canonical) }} 8 | 9 | {% if webmaster.email|default(null) and webmaster.name|default(null) %} 10 | {{ webmaster.email }} ({{ webmaster.name }}) 11 | {% endif %} 12 | {% set pubDate = pubDate|default((items|first).pubDate) %} 13 | {% if pubDate %} 14 | {{ pubDate|date(constant('DateTime::RFC2822')) }} 15 | {% endif %} 16 | {% if image|default(null) %} 17 | 18 | {{ image.url }} 19 | {{ title|default(title) }} 20 | {{ link|default(canonical) }} 21 | {{ description }} 22 | {{ image.width }} 23 | {{ image.height }} 24 | 25 | {% endif %} 26 | {% for item in items %} 27 | 28 | {{ item.title }} 29 | {{ item.link }} 30 | {{ item.description }} 31 | {% if item.guid|default(null) %} 32 | {{ item.guid }} 33 | {% endif %} 34 | {% if item.pubDate|default(null) %} 35 | {{ item.pubDate|date(constant('DateTime::RFC2822')) }} 36 | {% endif %} 37 | 38 | {% endfor %} 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/Resources/views/sitemap.xml.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% for url in sitemap %} 4 | 5 | {{ url.location }} 6 | {% if url.lastModified is defined %}{{ url.lastModified|date('c') }}{% endif %} 7 | {% if url.priority is defined %}{{ url.priority }}{% endif %} 8 | {{ url.frequency|default('monthly') }} 9 | 10 | {% endfor %} 11 | 12 | -------------------------------------------------------------------------------- /src/Routing/Route.php: -------------------------------------------------------------------------------- 1 | setFilePath($matches[1]); 21 | $this->setFilename($matches[2]); 22 | $this->format($matches[3]); 23 | } else { 24 | $this->setFilePath($pattern); 25 | } 26 | 27 | return $this; 28 | } 29 | 30 | /** 31 | * Set file path 32 | * 33 | * @param string $filePath 34 | * 35 | * @return Route 36 | */ 37 | public function setFilePath($filePath) 38 | { 39 | $this->setOption('filePath', $filePath); 40 | 41 | return $this; 42 | } 43 | 44 | /** 45 | * Get file path 46 | * 47 | * @return string 48 | */ 49 | public function getFilePath() 50 | { 51 | return $this->getOption('filePath'); 52 | } 53 | 54 | /** 55 | * Set file name 56 | * 57 | * @param string $fileName 58 | * 59 | * @return Route 60 | */ 61 | public function setFileName($fileName) 62 | { 63 | $this->setOption('fileName', $fileName); 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * Get file name 70 | * 71 | * @return string 72 | */ 73 | public function getFileName() 74 | { 75 | return $this->getOption('fileName') ?: 'index'; 76 | } 77 | 78 | /** 79 | * Content 80 | * 81 | * @param string $content 82 | * 83 | * @return Route 84 | */ 85 | public function content($content) 86 | { 87 | $this->setOption('content', $content); 88 | 89 | return $this; 90 | } 91 | 92 | /** 93 | * Contents 94 | * 95 | * @param string $content Type of content to load 96 | * @param string $index Index the results by the given field name 97 | * @param string $order Sort content: true for ascending, false for descending 98 | * 99 | * @return Route 100 | */ 101 | public function contents($content, $index = null, $order = true) 102 | { 103 | $this->content($content); 104 | 105 | $this->setOption('list', true); 106 | $this->setOption('index', $index); 107 | $this->setOption('order', $order); 108 | 109 | return $this; 110 | } 111 | 112 | /** 113 | * Paginate 114 | * 115 | * @param string $content 116 | * 117 | * @return Route 118 | */ 119 | public function paginate($content, $index = null, $order = true, $perPage = 10) 120 | { 121 | if (!$this->isPaginated()) { 122 | $this 123 | ->contents($content, $index, $order) 124 | ->setPerPage($perPage) 125 | ->setPath($this->getPath() . '/{page}') 126 | ->value('page', 1) 127 | ->assert('page', '\d+'); 128 | } 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * Get content 135 | * 136 | * @return string 137 | */ 138 | public function getContent() 139 | { 140 | return $this->getOption('content'); 141 | } 142 | 143 | /** 144 | * Has content? 145 | * 146 | * @return boolean 147 | */ 148 | public function hasContent() 149 | { 150 | return $this->hasOption('content'); 151 | } 152 | 153 | /** 154 | * Is content list? 155 | * 156 | * @return boolean 157 | */ 158 | public function isList() 159 | { 160 | return $this->getOption('list'); 161 | } 162 | 163 | /** 164 | * Get index by 165 | * 166 | * @return string 167 | */ 168 | public function getIndexBy() 169 | { 170 | return $this->getOption('index'); 171 | } 172 | 173 | /** 174 | * Get sort order 175 | * 176 | * @return boolean 177 | */ 178 | public function getOrder() 179 | { 180 | return $this->getOption('order'); 181 | } 182 | 183 | /** 184 | * Is pagination enabled? 185 | * 186 | * @return boolean 187 | */ 188 | public function isPaginated() 189 | { 190 | return $this->hasDefault('page'); 191 | } 192 | 193 | /** 194 | * Hide 195 | * 196 | * @return Route 197 | */ 198 | public function hide() 199 | { 200 | $this->setOption('hidden', true); 201 | 202 | return $this; 203 | } 204 | 205 | /** 206 | * Is visible? 207 | * 208 | * @return boolean 209 | */ 210 | public function isVisible() 211 | { 212 | return !$this->getOption('hidden'); 213 | } 214 | 215 | /** 216 | * Hide from sitemap 217 | * 218 | * @return Route 219 | */ 220 | public function hideFromSitemap() 221 | { 222 | $this->setOption('hide-from-sitemap', true); 223 | 224 | return $this; 225 | } 226 | 227 | /** 228 | * Is route on sitemap? 229 | * 230 | * @return boolean 231 | */ 232 | public function isMapped() 233 | { 234 | return $this->isVisible() && !$this->getOption('hide-from-sitemap'); 235 | } 236 | 237 | /** 238 | * Format 239 | * 240 | * @param string $format 241 | * 242 | * @return Route 243 | */ 244 | public function format($format) 245 | { 246 | $this 247 | ->value('_format', $format) 248 | ->assert('_format', $format); 249 | 250 | return $this; 251 | } 252 | 253 | /** 254 | * Set template 255 | * 256 | * @param string $template 257 | * 258 | * @return Route 259 | */ 260 | public function template($template) 261 | { 262 | $this->value('_template', $template); 263 | 264 | return $this; 265 | } 266 | 267 | /** 268 | * Set number of contents per page 269 | * 270 | * @param integer $perPage 271 | * 272 | * @return Route 273 | */ 274 | public function setPerPage($perPage) 275 | { 276 | $this->setOption('perPage', $perPage); 277 | 278 | return $this; 279 | } 280 | 281 | /** 282 | * Get number of contents per page 283 | * 284 | * @return integer 285 | */ 286 | public function getPerPage() 287 | { 288 | return $this->getOption('perPage') ?: 10; 289 | } 290 | 291 | /** 292 | * Get format 293 | * 294 | * @return string 295 | */ 296 | public function getFormat() 297 | { 298 | return $this->getDefault('_format') ?: 'html'; 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/Service/ContentRepository.php: -------------------------------------------------------------------------------- 1 | decoder = $decoder; 69 | $this->directory = rtrim($directory, '/'); 70 | $this->files = new FileSystem(); 71 | $this->handlers = []; 72 | $this->cache = [ 73 | 'files' => [], 74 | 'contents' => [], 75 | ]; 76 | } 77 | 78 | /** 79 | * Get all contents for the given type 80 | * 81 | * @param string $type Type of content to load 82 | * @param string|null $index Index the result array by the given field name (from content) 83 | * @param boolean|null $order Sort the result array: true for ascending, false for descending 84 | * 85 | * @return array[] Array of contents 86 | */ 87 | public function getContents($type, $index = null, $order = true) 88 | { 89 | $contents = []; 90 | $files = $this->listFiles($type); 91 | 92 | foreach ($files as $file) { 93 | $content = $this->load($file); 94 | $contents[$this->getIndex($file, $content, $index)] = $content; 95 | } 96 | 97 | if ($order !== null) { 98 | $order ? ksort($contents) : krsort($contents); 99 | } 100 | 101 | return $contents; 102 | } 103 | 104 | /** 105 | * List of content names for the given type 106 | * 107 | * @param string $type Type of content to list 108 | * 109 | * @return string[] Content names 110 | */ 111 | public function listContents($type) 112 | { 113 | $names = []; 114 | $files = $this->listFiles($type); 115 | 116 | foreach ($files as $file) { 117 | $names[] = static::getName($file); 118 | } 119 | 120 | return $names; 121 | } 122 | 123 | /** 124 | * Get the content for the given type and name 125 | * 126 | * @param string $type Type of content to load 127 | * @param string $name The name of the content file (without extension) 128 | * 129 | * @return array Content as associative array 130 | */ 131 | public function getContent($type, $name) 132 | { 133 | $finder = $this->listFiles($type)->name($name . '.*'); 134 | 135 | if (!$finder->count()) { 136 | throw new Exception(sprintf( 137 | 'No content directory find for type "%s" and name "%s" (in "%s").', 138 | $type, 139 | $name, 140 | $this->directory 141 | )); 142 | } 143 | 144 | foreach ($finder as $file) { 145 | return $this->load($file); 146 | } 147 | 148 | return null; 149 | } 150 | 151 | /** 152 | * Add property handler 153 | * 154 | * @param PropertyHandlerInterface $handler Handler 155 | * 156 | * @return ContentRepository 157 | */ 158 | public function addPropertyHandler(PropertyHandlerInterface $handler) 159 | { 160 | $this->handlers[$handler->getProperty()] = $handler; 161 | 162 | return $this; 163 | } 164 | 165 | /** 166 | * Get the name of a file 167 | * 168 | * @param SplFileInfo $file The file 169 | * 170 | * @return string The name 171 | */ 172 | public static function getName(SplFileInfo $file) 173 | { 174 | $name = $file->getRelativePathname(); 175 | 176 | return substr($name, 0, strrpos($name, '.')); 177 | } 178 | 179 | /** 180 | * Get the format of a file from its extension 181 | * 182 | * @param SplFileInfo $file The file 183 | * 184 | * @return string The format 185 | */ 186 | public static function getFormat(SplFileInfo $file) 187 | { 188 | $name = $file->getRelativePathname(); 189 | $ext = substr($name, strrpos($name, '.') + 1); 190 | 191 | switch ($ext) { 192 | case 'md': 193 | return 'markdown'; 194 | 195 | case 'yml': 196 | return 'yaml'; 197 | 198 | default: 199 | return $ext; 200 | } 201 | } 202 | 203 | /** 204 | * List files for the given type 205 | * 206 | * @param string $type Type of content to list 207 | * 208 | * @return Finder A Finder instance, filtered by type 209 | */ 210 | protected function listFiles($type) 211 | { 212 | if (!isset($this->cache['files'][$type])) { 213 | $path = sprintf('%s/%s', $this->directory, $type); 214 | 215 | if (!$this->files->exists($path)) { 216 | throw new Exception(sprintf( 217 | 'No content directory found for type "%s" (in "%s").', 218 | $type, 219 | $this->directory 220 | )); 221 | } 222 | 223 | $finder = new Finder(); 224 | 225 | $this->cache['files'][$type] = $finder->files()->in($path); 226 | } 227 | 228 | return clone $this->cache['files'][$type]; 229 | } 230 | 231 | /** 232 | * Get index of the given content for content lists 233 | * 234 | * @param SplFileInfo $file 235 | * @param array $content 236 | * @param string|null $key 237 | * 238 | * @return string The string index (by default, the file name) 239 | */ 240 | protected function getIndex(SplFileInfo $file, $content, $key = null) 241 | { 242 | if ($key === null || !isset($content[$key])) { 243 | return static::getName($file); 244 | } 245 | 246 | $index = $content[$key]; 247 | 248 | if ($index instanceof DateTime) { 249 | return $index->format('U'); 250 | } 251 | 252 | return (string) $index; 253 | } 254 | 255 | /** 256 | * Get the file content 257 | * 258 | * @param SplFileInfo $file The file to load 259 | * 260 | * @return array Parsed content (associative array) 261 | */ 262 | protected function load(SplFileInfo $file) 263 | { 264 | $path = $file->getPathName(); 265 | 266 | if (!isset($this->cache['contents'][$path])) { 267 | $data = $this->decoder->decode($file->getContents(), static::getFormat($file)); 268 | $context = ['file' => $file, 'data' => $data]; 269 | 270 | if (is_array($data)) { 271 | foreach ($this->handlers as $property => $handler) { 272 | if ($handler->isSupported($data)) { 273 | $value = isset($data[$property]) ? $data[$property] : null; 274 | $data[$property] = $handler->handle($value, $context); 275 | } 276 | } 277 | } 278 | 279 | $this->cache['contents'][$path] = $data; 280 | } 281 | 282 | return $this->cache['contents'][$path]; 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/Service/Informator.php: -------------------------------------------------------------------------------- 1 | urlGenerator = $urlGenerator; 29 | } 30 | 31 | /** 32 | * Before request 33 | * 34 | * @param Request $request 35 | * @param Application $app 36 | */ 37 | public function beforeRequest(Request $request, Application $app) 38 | { 39 | if ($canonical = $this->getCanonicalUrl($request)) { 40 | $request->attributes->set('_canonical', $canonical); 41 | $app['twig']->addGlobal('canonical', $canonical); 42 | } 43 | 44 | if ($root = $this->getRootUrl($request)) { 45 | $request->attributes->set('_root', $root); 46 | $app['twig']->addGlobal('root', $root); 47 | } 48 | } 49 | 50 | /** 51 | * Get canonical URL 52 | * 53 | * @param Request $request 54 | * 55 | * @return string 56 | */ 57 | protected function getCanonicalUrl(Request $request) 58 | { 59 | return $this->urlGenerator->generate( 60 | $request->attributes->get('_route'), 61 | $request->attributes->get('_route_params'), 62 | UrlGeneratorInterface::ABSOLUTE_URL 63 | ); 64 | } 65 | 66 | /** 67 | * Get root URL 68 | * 69 | * @param Request $request 70 | * 71 | * @return string 72 | */ 73 | protected function getRootUrl(Request $request) 74 | { 75 | return sprintf('%s://%s', $request->getScheme(), $request->getHost()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Service/Parsedown.php: -------------------------------------------------------------------------------- 1 | pygments = $pygments; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | protected function blockCodeComplete($Block) 34 | { 35 | $Block['element']['text']['text'] = $this->getCode($Block); 36 | 37 | return $Block; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | protected function blockFencedCodeComplete($Block) 44 | { 45 | $Block['element']['text']['text'] = $this->getCode($Block); 46 | 47 | return $Block; 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | protected function inlineLink($Excerpt) 54 | { 55 | $data = parent::inlineLink($Excerpt); 56 | 57 | if (preg_match('#(https?:)?//#i', $data['element']['attributes']['href'])) { 58 | $data['element']['attributes']['target'] = '_blank'; 59 | } 60 | 61 | return $data; 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | protected function blockHeader($Line) 68 | { 69 | $Block = parent::blockHeader($Line); 70 | 71 | $Block['element']['attributes']['id'] = $this->getId($Block); 72 | 73 | return $Block; 74 | } 75 | 76 | /** 77 | * Process code content 78 | * 79 | * @param string $text 80 | * 81 | * @return string 82 | */ 83 | protected function getCode($Block) 84 | { 85 | if (!isset($Block['element']['text']['text'])) { 86 | return null; 87 | } 88 | 89 | $text = $Block['element']['text']['text']; 90 | 91 | if ($this->pygments && $language = $this->getLanguage($Block)) { 92 | return $this->pygments->highlight($text, $language); 93 | } 94 | 95 | return htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); 96 | } 97 | 98 | /** 99 | * Get language of the given block 100 | * 101 | * @param array $Block 102 | * 103 | * @return string 104 | */ 105 | protected function getLanguage($Block) 106 | { 107 | if (!isset($Block['element']['text']['attributes'])) { 108 | return null; 109 | } 110 | 111 | return substr($Block['element']['text']['attributes']['class'], strlen('language-')); 112 | } 113 | 114 | /** 115 | * Get ID for the given block 116 | * 117 | * @param array $Block 118 | * 119 | * @return string 120 | */ 121 | protected function getId($Block) 122 | { 123 | return preg_replace('#[^a-z]+#i', '-', strtolower($Block['element']['text'])); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Service/Pygments.php: -------------------------------------------------------------------------------- 1 | tmp = $tmp ?: sys_get_temp_dir(); 36 | $this->files = new Filesystem(); 37 | } 38 | 39 | /** 40 | * Highlight a portion of code with pygmentize 41 | * 42 | * @param string $value 43 | * @param string $language 44 | * 45 | * @return string 46 | */ 47 | public function highlight($value, $language) 48 | { 49 | $path = tempnam($this->tmp, 'pyg'); 50 | 51 | if ($language === 'php' && substr($value, 0, 5) !== 'files->dumpFile($path, $value); 56 | 57 | $value = $this->pygmentize($path, $language); 58 | 59 | unlink($path); 60 | 61 | if (preg_match('#^
#', $value) && preg_match('#
$#', $value)) { 62 | return substr($value, 28, strlen($value) - 40); 63 | } 64 | 65 | return $value; 66 | } 67 | 68 | /** 69 | * Run 'pygmentize' command on the given file 70 | * 71 | * @param string $path 72 | * @param string $language 73 | * 74 | * @return string 75 | */ 76 | public function pygmentize($path, $language) 77 | { 78 | $process = new Process(sprintf('pygmentize -f html -l %s %s', $language, $path)); 79 | 80 | $process->run(); 81 | 82 | if (!$process->isSuccessful()) { 83 | throw new RuntimeException($process->getErrorOutput()); 84 | } 85 | 86 | return trim($process->getOutput()); 87 | } 88 | 89 | /** 90 | * Is pygmentize available? 91 | * 92 | * @return boolean 93 | */ 94 | public static function isAvailable() 95 | { 96 | $process = new Process('pygmentize -V'); 97 | 98 | $process->run(); 99 | 100 | return $process->isSuccessful(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Twig/MarkdownExtension.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function getFilters() 35 | { 36 | return [ 37 | new SimpleFilter('markdown', [$this, 'markdownify']), 38 | ]; 39 | } 40 | 41 | /** 42 | * Parse Mardown to return HTML 43 | * 44 | * @param string $data 45 | * 46 | * @return string 47 | */ 48 | public function markdownify($data) 49 | { 50 | return $this->parser->parse($data); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function getName() 57 | { 58 | return 'markdown_extension'; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Twig/PublicExtension.php: -------------------------------------------------------------------------------- 1 | root = $request->attributes->get('_root'); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function getFunctions() 37 | { 38 | return [ 39 | new SimpleFunction('public', [$this, 'getPublicUrl']), 40 | ]; 41 | } 42 | 43 | /** 44 | * Get public url for the given path 45 | * 46 | * @param string $path The path to expose 47 | * @param boolean $absolute Whether or not the url should be absolute 48 | * 49 | * @return string 50 | */ 51 | public function getPublicUrl($path, $absolute = false) 52 | { 53 | return sprintf('%s/%s', $absolute ? $this->root : null, ltrim($path, '/')); 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function getName() 60 | { 61 | return 'public_extension'; 62 | } 63 | } 64 | --------------------------------------------------------------------------------