commit 09139d2b14a742335472ea4d5f656caaf8ecaa0b Author: Michał Brzuchalski Date: Fri Jun 10 13:19:45 2016 +0200 Initial commit diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..35bf87e --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,4 @@ +# for php-coveralls +src_dir: src +coverage_clover: tests/coverage/clover.xml +json_path: tests/coverage/coveralls-upload.json \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bfc3e08 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +/app/config/parameters.yml +/build/ +/var/* +!/var/.gitkeep +/vendor/ +/web/bundles/ +/.sonar/ +/.idea/ +/bin/* +!/bin/console +!/bin/symfony_requirements +/tests/coverage/* +!tests/coverage/.gitkeep +/tests/pdepend/* +!tests/pdepend/.gitkeep +/docker/rebuild/build/* +!/docker/rebuild/build/.gitkeep +/docker/rebuild/partials/* +!/docker/rebuild/partials/.gitkeep +/docker/rebuild/resources/* +!/docker/rebuild/resources/.gitkeep \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2b4fc61 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Madkom S.A. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..aa3346e --- /dev/null +++ b/README.md @@ -0,0 +1,233 @@ +NGINX Configurator +================== + +PHP Library for NGINX configuration parser/generator + +![PHP 7.0](https://img.shields.io/badge/PHP-7.0-8C9CB6.svg?style=flat) +[![Build Status](https://travis-ci.org/madkom/nginx-configurator.svg?branch=master)](https://travis-ci.org/madkom/nginx-configurator) +[![Latest Stable Version](https://poser.pugx.org/madkom/nginx-configurator/v/stable)](https://packagist.org/packages/madkom/nginx-configurator) +[![Total Downloads](https://poser.pugx.org/madkom/nginx-configurator/downloads)](https://packagist.org/packages/madkom/nginx-configurator) +[![License](https://poser.pugx.org/madkom/nginx-configurator/license)](https://packagist.org/packages/madkom/nginx-configurator) +[![Coverage Status](https://coveralls.io/repos/github/madkom/nginx-configurator/badge.svg?branch=master)](https://coveralls.io/github/madkom/nginx-configurator?branch=master) +[![Code Climate](https://codeclimate.com/github/madkom/nginx-configurator/badges/gpa.svg)](https://codeclimate.com/github/madkom/nginx-configurator) +[![Issue Count](https://codeclimate.com/github/madkom/nginx-configurator/badges/issue_count.svg)](https://codeclimate.com/github/madkom/nginx-configurator) + +--- + +## Features + +This library can parse and generate NGINX configuration files. + + +## Installation + +Install with Composer + +``` +composer require madkom/nginx-configurator +``` + +## Requirements + +This library requires *PHP* in `~7` version. + +## Usage + +Parsing configuration string: + +```php +parse($config); +/** @var Server $defaultServers[] */ +$defaultServers = $defaultConfig->search(function (Node $node) { + return $node instanceof Server; +}); + + +$builder = new Builder(); +if (count($defaultServers) > 0) { + /** @var Server $defaultServer */ + foreach ($defaultServers as $defaultServer) { + $builder->appendServerNode($defaultServer); + } +} +``` + +Generating configuration string: + +```php +addServerNode(80); +$server->append(new Directive('error_log', [new Param('/var/log/nginx/error.log'), new Param('debug')])); +$server->append(new Location(new Param('/test'), null, [ + new Directive('error_page', [new Param('401'), new Param('@unauthorized')]), + new Directive('set', [new Param('$auth_user'), new Literal('none')]), + new Directive('auth_request', [new Param('/auth')]), + new Directive('proxy_pass', [new Param('http://test-service')]), +])); +$server->append(new Location(new Param('/auth'), null, [ + new Directive('proxy_pass', [new Param('http://auth-service:9999')]), + new Directive('proxy_bind', [new Param('$server_addr')]), + new Directive('proxy_redirect', [new Param('http://$host'), new Param('https://$host')]), + new Directive('proxy_set_header', [new Param('Content-Length'), new Literal("")]), + new Directive('proxy_pass_request_body', [new Param('off')]), +])); +$server->append(new Location(new Param('@unauthorized'), null, [ + new Directive('return', [new Param('302'), new Param('/login?backurl=$request_uri')]), +])); +$server->append(new Location(new Param('/login'), null, [ + new Directive('expires', [new Param('-1')]), + new Directive('proxy_pass', [new Param('http://identity-provider-service')]), + new Directive('proxy_bind', [new Param('$server_addr')]), + new Directive('proxy_redirect', [new Param('http://$host'), new Param('https://$host')]), + new Directive('proxy_set_header', [new Param('Content-Length'), new Literal("")]), + new Directive('proxy_pass_request_body', [new Param('off')]), +])); + +print($builder->dump()); +``` + +Generated configuration output: + +``` +server { + listen 80; + listen [::]:80 default ipv6only=on; + error_log /var/log/nginx/error.log debug; + location /test { + error_page 401 @unauthorized; + set $auth_user "none"; + auth_request /auth; + proxy_pass http://test-service; + } + + location /auth { + proxy_pass http://auth-service:9999; + proxy_bind $server_addr; + proxy_redirect http://$host https://$host; + proxy_set_header Content-Length ""; + proxy_pass_request_body off; + } + + location @unauthorized { + return 302 /login?backurl=$request_uri; + } + + location /login { + expires -1; + proxy_pass http://identity-provider-service; + proxy_bind $server_addr; + proxy_redirect http://$host https://$host; + proxy_set_header Content-Length ""; + proxy_pass_request_body off; + } + +} +``` + +There are also methods to read and dump file: + +```php + +use Madkom\NginxConfigurator\Builder; +use Madkom\NginxConfigurator\Config\Location; +use Madkom\NginxConfigurator\Config\Server; +use Madkom\NginxConfigurator\Node\Directive; +use Madkom\NginxConfigurator\Node\Literal; +use Madkom\NginxConfigurator\Parser; + +require __DIR__ . '/../vendor/autoload.php'; + +$parser = new Parser(); +$builder = new Builder(); + +$configuration = $parser->parseFile('default.conf'); + +/** @var Server $servers[] */ +$servers = $configuration->search(function (Node $node) { + return $node instanceof Server; +}); +if (count($servers) > 0) { + /** @var Server $server */ + foreach ($servers as $server) { + $builder->appendServerNode($server); + } +} + +$builder->dumpFile('generated.conf'); +``` + +## TODO + +* [ ] Implement comments parsing + +## License + +The MIT License (MIT) + +Copyright (c) 2016 Madkom S.A. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a99bad2 --- /dev/null +++ b/composer.json @@ -0,0 +1,35 @@ +{ + "name": "madkom/nginx-configurator", + "license": "MIT", + "homepage": "http://madkom.pl/", + "minimum-stability": "dev", + "require": { + "madkom/collection": "^1.0", + "ferno/loco": "@dev", + "madkom/uri": "^1.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.5", + "phpunit/phpunit": "~4" + }, + "repositories": [ + { + "type": "vcs", + "url": "git@github.com:madkom/loco.git" + } + ], + "autoload": { + "psr-4": { + "Madkom\\NginxConfigurator\\": "src/" + } + }, + "authors": [ + { + "name": "Michał Brzuchalski", + "email": "m.brzuchalski@madkom.pl" + } + ], + "config": { + "bin-dir": "bin/" + } +} diff --git a/examples/build.php b/examples/build.php new file mode 100644 index 0000000..0174927 --- /dev/null +++ b/examples/build.php @@ -0,0 +1,40 @@ +addServerNode(80); +$server->append(new Directive('error_log', [new Param('/var/log/nginx/error.log'), new Param('debug')])); +$server->append(new Location(new Param('/test'), null, [ + new Directive('error_page', [new Param('401'), new Param('@unauthorized')]), + new Directive('set', [new Param('$auth_user'), new Literal('none')]), + new Directive('auth_request', [new Param('/auth')]), + new Directive('proxy_pass', [new Param('http://test-service')]), +])); +$server->append(new Location(new Param('/auth'), null, [ + new Directive('proxy_pass', [new Param('http://auth-service:9999')]), + new Directive('proxy_bind', [new Param('$server_addr')]), + new Directive('proxy_redirect', [new Param('http://$host'), new Param('https://$host')]), + new Directive('proxy_set_header', [new Param('Content-Length'), new Literal("")]), + new Directive('proxy_pass_request_body', [new Param('off')]), +])); +$server->append(new Location(new Param('@unauthorized'), null, [ + new Directive('return', [new Param('302'), new Param('/login?backurl=$request_uri')]), +])); +$server->append(new Location(new Param('/login'), null, [ + new Directive('expires', [new Param('-1')]), + new Directive('proxy_pass', [new Param('http://identity-provider-service')]), + new Directive('proxy_bind', [new Param('$server_addr')]), + new Directive('proxy_redirect', [new Param('http://$host'), new Param('https://$host')]), + new Directive('proxy_set_header', [new Param('Content-Length'), new Literal("")]), + new Directive('proxy_pass_request_body', [new Param('off')]), +])); + +print($builder->dump()); \ No newline at end of file diff --git a/examples/default.conf b/examples/default.conf new file mode 100644 index 0000000..6c40142 --- /dev/null +++ b/examples/default.conf @@ -0,0 +1,25 @@ +server { + listen 8080; + root /data/www/web; + index index.php index.html index.htm; + + location / { + try_files $uri $uri/ /index.php; + } + + error_page 404 /404.html; + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/www; + } + + # pass the PHP scripts to FastCGI server listening on the php-fpm socket + location ~ \.php$ { + try_files $uri =404; + fastcgi_pass unix:/var/run/php5-fpm.sock; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + } +} diff --git a/examples/parse.php b/examples/parse.php new file mode 100644 index 0000000..9280dcf --- /dev/null +++ b/examples/parse.php @@ -0,0 +1,54 @@ +parse($config); + +/** @var Server $defaultServers[] */ +$defaultServers = $defaultConfig->search(function (Node $node) { + return $node instanceof Server; +}); + + + +$builder = new Builder(); +if (count($defaultServers) > 0) { + /** @var Server $defaultServer */ + foreach ($defaultServers as $defaultServer) { + $builder->appendServerNode($defaultServer); + } +} diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..7668727 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,11 @@ + + + Project Coding Standard + ./src + + + + + + + \ No newline at end of file diff --git a/phpspec.yml b/phpspec.yml new file mode 100644 index 0000000..3db9e3b --- /dev/null +++ b/phpspec.yml @@ -0,0 +1,5 @@ +suites: + types: + namespace: Madkom\NginxConfigurator + psr4_prefix: Madkom\NginxConfigurator + spec_path: tests \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..bf027e9 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,27 @@ + + + + + ./tests/phpunit/ + ./vendor + ./app + + + + + ./vendor + ./app + ./bin + ./tests + ./var/bootstrap.php.cache + + + + + + + \ No newline at end of file diff --git a/src/Builder.php b/src/Builder.php new file mode 100644 index 0000000..4bfbe00 --- /dev/null +++ b/src/Builder.php @@ -0,0 +1,93 @@ + + */ +class Builder +{ + /** + * @var RootNode Holds configuration root node + */ + protected $rootNode; + + /** + * Builder constructor. + */ + public function __construct() + { + $this->clear(); + } + + public function clear() + { + $this->rootNode = new RootNode(); + } + + /** + * @param int $port + * @return Server + */ + public function addServerNode(int $port) : Server + { + $listenIPv4 = new Directive('listen', [new Param($port)]); + $listenIPv6 = new Directive('listen', [new Param("[::]:{$port}"), new Param('default'), new Param('ipv6only=on')]); + $httpNode = new Server([$listenIPv4, $listenIPv6]); + $this->rootNode->append($httpNode); + + return $httpNode; + } + + /** + * @param Server $server + * @return Server + */ + public function appendServerNode(Server $server) : Server + { + $this->rootNode->append($server); + + return $server; + } + + /** + * @param Upstream $upstream + * @return Upstream + */ + public function appendUpstreamNode(Upstream $upstream) : Upstream + { + $this->rootNode->append($upstream); + + return $upstream; + } + + /** + * @return string + */ + public function dump() : string + { + return (string)$this->rootNode; + } + + /** + * @param string $filename + * @return bool + */ + public function dumpFile(string $filename) : bool + { + return file_put_contents($filename, $this->dump()); + } +} diff --git a/src/Config/Events.php b/src/Config/Events.php new file mode 100644 index 0000000..97a66e4 --- /dev/null +++ b/src/Config/Events.php @@ -0,0 +1,28 @@ + + */ +class Events extends Context +{ + /** + * Events constructor. + * @param Directive[] $directives + */ + public function __construct(array $directives = []) + { + parent::__construct('events', $directives); + } +} diff --git a/src/Config/Http.php b/src/Config/Http.php new file mode 100644 index 0000000..1e02625 --- /dev/null +++ b/src/Config/Http.php @@ -0,0 +1,28 @@ + + */ +class Http extends Context +{ + /** + * Http constructor. + * @param Directive[] $directives + */ + public function __construct(array $directives = []) + { + parent::__construct('http', $directives); + } +} diff --git a/src/Config/Location.php b/src/Config/Location.php new file mode 100644 index 0000000..30b4176 --- /dev/null +++ b/src/Config/Location.php @@ -0,0 +1,71 @@ + + */ +class Location extends Context +{ + /** + * Holds location match + * @var Param + */ + private $location; + + /** + * Holds location match modifier + * @var Param + */ + private $match; + + /** + * Location constructor. + * @param Param $location + * @param Param $match + * @param array $directives + */ + public function __construct(Param $location, Param $match = null, array $directives = []) + { + $this->location = $location; + $this->match = $match; + parent::__construct('location', $directives); + } + + public function __toString() : string + { + return sprintf( + "{$this->name} %s %s {\n\t%s\n}\n", + $this->match, + $this->location, + implode("\n\t", (array)$this->childNodes->getIterator()) + ); + } + + /** + * @return Param + */ + public function getLocation() : string + { + return (string)$this->location; + } + + /** + * @return Param + */ + public function getMatch() : string + { + return (string)$this->match; + } +} diff --git a/src/Config/Server.php b/src/Config/Server.php new file mode 100644 index 0000000..2310a86 --- /dev/null +++ b/src/Config/Server.php @@ -0,0 +1,27 @@ + + */ +class Server extends Context +{ + /** + * Server constructor. + * @param array $directives + */ + public function __construct(array $directives = []) + { + parent::__construct('server', $directives); + } +} diff --git a/src/Config/Upstream.php b/src/Config/Upstream.php new file mode 100644 index 0000000..7dbc1e4 --- /dev/null +++ b/src/Config/Upstream.php @@ -0,0 +1,51 @@ + + */ +class Upstream extends Context +{ + /** + * Holds upstream name + * @var Param + */ + private $upstream; + + /** + * Upstream constructor. + * @param Param $upstream + * @param Directive[] $directives + */ + public function __construct(Param $upstream, array $directives = []) + { + $this->upstream = $upstream; + parent::__construct('upstream', $directives); + } + + public function getName() : string + { + return (string)$this->upstream->getValue(); + } + + public function __toString() : string + { + return sprintf( + "{$this->name} %s {\n\t%s\n}\n", + $this->upstream, + implode("\n\t", (array)$this->childNodes->getIterator()) + ); + } +} diff --git a/src/Exception/GrammarException.php b/src/Exception/GrammarException.php new file mode 100644 index 0000000..70d93f2 --- /dev/null +++ b/src/Exception/GrammarException.php @@ -0,0 +1,20 @@ + + */ +class GrammarException extends Exception +{ + +} diff --git a/src/Exception/UnrecognizedContextException.php b/src/Exception/UnrecognizedContextException.php new file mode 100644 index 0000000..4d10dbb --- /dev/null +++ b/src/Exception/UnrecognizedContextException.php @@ -0,0 +1,20 @@ + + */ +class UnrecognizedContextException extends Exception +{ + +} diff --git a/src/Node/Context.php b/src/Node/Context.php new file mode 100644 index 0000000..14ae923 --- /dev/null +++ b/src/Node/Context.php @@ -0,0 +1,42 @@ + + */ +abstract class Context extends Node +{ + /** + * Context constructor. + * @param string $name + * @param Directive[] $directives + */ + public function __construct($name, array $directives = []) + { + parent::__construct($name); + foreach ($directives as $directive) { + $this->append($directive); + } + } + + public function __toString() : string + { + $childStrings = []; + foreach ($this->childNodes as $childNode) { + $childStrings[] = implode("\n\t", explode("\n", (string)$childNode)); + } + + return sprintf( + "{$this->name} {\n\t%s\n}\n", + implode("\n\t", $childStrings) + ); + } +} diff --git a/src/Node/Directive.php b/src/Node/Directive.php new file mode 100644 index 0000000..1bd8d7d --- /dev/null +++ b/src/Node/Directive.php @@ -0,0 +1,77 @@ + + */ +class Directive extends Node +{ + /** + * Holds directive name + * @var string + */ + protected $name; + /** + * Holds param collection + * @var CustomTypedCollection|Param[] + */ + protected $params; + + /** + * Directive constructor. + * @param string $name + * @param array $params + */ + public function __construct(string $name, array $params = []) + { + parent::__construct($name); + $this->params = new class($params) extends CustomTypedCollection { + + /** + * Retrieves collection type + * @return string + */ + protected function getType() : string + { + return Param::class; + } + }; + } + + /** + * Retrieve directive name + * @return string + */ + public function getName() : string + { + return $this->name; + } + + /** + * Retrieve params iterator + * @return Traversable|Param[] + */ + public function getParams() : Traversable + { + return $this->params->getIterator(); + } + + /** + * @return string + */ + public function __toString() : string + { + return sprintf("{$this->name} %s;", implode(' ', (array)$this->params->getIterator())); + } +} diff --git a/src/Node/Literal.php b/src/Node/Literal.php new file mode 100644 index 0000000..a85af02 --- /dev/null +++ b/src/Node/Literal.php @@ -0,0 +1,24 @@ + + */ +class Literal extends Param +{ + /** + * @return string + */ + public function __toString() : string + { + return '"' . addslashes($this->value) . '"'; + } +} diff --git a/src/Node/Node.php b/src/Node/Node.php new file mode 100644 index 0000000..4ded3be --- /dev/null +++ b/src/Node/Node.php @@ -0,0 +1,122 @@ + + */ +abstract class Node implements Countable, IteratorAggregate +{ + /** + * Holds parent node + * @var Node + */ + protected $parent; + /** + * Holds node name + * @var string + */ + protected $name = ''; + /** + * Holds node children + * @var CustomTypedCollection + */ + protected $childNodes; + + /** + * Node constructor. + * @param string $name + */ + public function __construct(string $name) + { + $this->name = $name; + $this->childNodes = new class extends CustomTypedCollection { + /** + * Retrieves collection type + * @return string + */ + protected function getType() : string + { + return Node::class; + } + }; + } + + /** + * Append new child node + * @param Node $node + * @return bool + */ + public function append(Node $node) : bool + { + $node->parent = $this; + + return $this->childNodes->add($node); + } + + /** + * Remove child node + * @param Node $node + * @return bool + */ + public function remove(Node $node) : bool + { + return $this->childNodes->remove($node); + } + + /** + * Search for specified nodes + * @param callable $checker + * @return CustomTypedCollection + */ + public function search(callable $checker) : CustomTypedCollection + { + return $this->childNodes->filter($checker); + } + + /** + * Count elements of an object + * @link http://php.net/manual/en/countable.count.php + * @return int The custom count as an integer. + *

+ *

+ * The return protocol is cast to an integer. + * @since 5.1.0 + */ + public function count() + { + return count($this->childNodes); + } + + /** + * Retrieve an external iterator + * @link http://php.net/manual/en/iteratoraggregate.getiterator.php + * @return Traversable An instance of an object implementing Iterator or + * Traversable + * @since 5.0.0 + */ + public function getIterator() + { + return $this->childNodes->getIterator(); + } + + /** + * @return string + */ + public function __toString() : string + { + return (string)implode("\n", (array)$this->childNodes->getIterator()); + } +} diff --git a/src/Node/Param.php b/src/Node/Param.php new file mode 100644 index 0000000..17561ab --- /dev/null +++ b/src/Node/Param.php @@ -0,0 +1,47 @@ + + */ +class Param +{ + /** + * @var string + */ + protected $value; + + /** + * Param constructor. + * @param string $value + */ + public function __construct(string $value) + { + $this->value = $value; + } + + /** + * Retrieve param value + * @return string + */ + public function getValue() : string + { + return $this->value; + } + + /** + * @return string + */ + public function __toString() : string + { + return $this->value; + } +} diff --git a/src/Node/RootNode.php b/src/Node/RootNode.php new file mode 100644 index 0000000..dc2391d --- /dev/null +++ b/src/Node/RootNode.php @@ -0,0 +1,28 @@ + + */ +class RootNode extends Node +{ + /** + * RootNode constructor. + * @param Node[] $nodes + */ + public function __construct(array $nodes = []) + { + parent::__construct(''); + foreach ($nodes as $node) { + $this->append($node); + } + } +} diff --git a/src/Parser.php b/src/Parser.php new file mode 100644 index 0000000..c8ced97 --- /dev/null +++ b/src/Parser.php @@ -0,0 +1,220 @@ + + */ +class Parser extends Grammar +{ + /** + * Holds parsed filename + * @var string + */ + protected $filename; + /** + * Holds parsed string + * @var string + */ + protected $content; + + /** + * Parser constructor. + */ + public function __construct() + { + parent::__construct('syntax', [ + 'syntax' => new GreedyStarParser(new LazyAltParser(['directive', 'section'])), + 'sections' => new GreedyMultiParser('section', 0, 2), + 'section' => new ConcParser( + [ + 'section-name', + new LazyAltParser(['space', 'opt-space']), + new LazyAltParser(['params', new LazyAltParser(['space', 'opt-space'])]), + new StringParser('{'), + new LazyAltParser(['space', 'opt-space']), + new GreedyMultiParser(new LazyAltParser(['directive', 'section']), 0, null), + new LazyAltParser(['space', 'opt-space']), + new StringParser('}'), + new LazyAltParser(['space', 'opt-space']), + ], + [$this, 'parseSection'] + ), + 'section-name' => new RegexParser('/^[a-z0-9\_]+/i'), + + 'directives' => new GreedyMultiParser('directive', 0, null), + 'directive' => new LazyAltParser([ + new ConcParser([ + 'directive-name', + 'semicolon', + new LazyAltParser(['space', 'opt-space']), + ], [$this, 'parseDirective']), + new ConcParser([ + 'directive-name', + 'space', + 'params', + 'semicolon', + new LazyAltParser(['space', 'opt-space']), + ], [$this, 'parseDirective']) + ]), + 'directive-name' => new RegexParser('/^[a-z0-9\_]+/i'), + + 'params' => new GreedyMultiParser(new ConcParser(['param', 'opt-space'], function ($param, $space) { + return $param; + }), 0, null), + 'param' => new LazyAltParser(['literal', 'param-name']), + 'param-name' => new RegexParser('/^[^\s\r\n\{\}\;\"\']+/i', function ($match) { + return new Param($match); + }), + 'literal' => new LazyAltParser([ + new RegexParser('/^"([^"]*)"/', function ($match0, $match1) { + return new Literal($match1); + }), + new RegexParser("/^'([^']*)'/", function ($match0, $match1) { + return new Literal($match1); + }) + ]), + + 'semicolon' => new StringParser(';', function () { + return null; + }), + 'space' => new GreedyStarParser('whitespace/comment', function () { + return null; + }), + 'whitespace/comment' => new LazyAltParser(['whitespace', 'comment'], function () { + return null; + }), + 'comment' => new RegexParser("/^#+([^\r\n]*)/", function () { + return null; + }), + 'whitespace' => new RegexParser("/^[ \t\r\n]+/"), + 'opt-space' => new RegexParser("/^[ \t\r\n]?/"), + 'eol' => new LazyAltParser([new StringParser("\r"), new StringParser("\n")], function () { + return null; + }) + ], function (array $nodes = []) { + return new RootNode($nodes); + }); + } + + /** + * Parses config file + * @param string $filename + * @return mixed + * @throws ParseFailureException + */ + public function parseFile(string $filename) : RootNode + { + $this->content = null; + $this->filename = $filename; + + return $this->parse(file_get_contents($filename)); + } + + /** + * Parses string + * @param string $string + * @return mixed + * @throws ParseFailureException + */ + public function parse($string) : RootNode + { + $this->content = $string; + $this->filename = null; + + return parent::parse($string); + } + + /** + * Parses section entries + * @param string $section Section name + * @param null $space0 Ignored + * @param Param[] $params Params collection + * @param null $open Ignored + * @param null $space1 Ignored + * @param Directive[] $directives Directives collection + * @return Context + * @throws GrammarException + * @throws UnrecognizedContextException + */ + protected function parseSection($section, $space0 = null, $params, $open = null, $space1 = null, $directives) : Context + { + switch ($section) { + case 'server': + return new Server($directives); + + case 'http': + return new Http($directives); + + case 'location': + $modifier = null; + if (sizeof($params) == 2) { + list($modifier, $location) = $params; + } elseif (sizeof($params) == 1) { + $location = $params[0]; + } else { + throw new GrammarException( + sprintf( + "Location context missing in %s", + $this->filename ? var_export($this->filename, true) : var_export($this->content, true) + ) + ); + } + return new Location($location, $modifier, $directives); + + case 'events': + return new Events($directives); + + case 'upstream': + list($upstream) = $params; + return new Upstream($upstream, $directives); + } + + throw new UnrecognizedContextException( + sprintf( + "Unrecognized context: {$section} found in %s", + $this->filename ? var_export($this->filename, true) : var_export($this->content, true) + ) + ); + } + + /** + * Parses directive + * @param string $name + * @param null $space + * @param array $params + * @return Directive + */ + protected function parseDirective(string $name, $space = null, $params = []) : Directive + { + return new Directive($name, is_null($params) ? [] : $params); + } +} diff --git a/tests/coverage/.gitkeep b/tests/coverage/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/spec/ParserSpec.php b/tests/spec/ParserSpec.php new file mode 100644 index 0000000..9aa5049 --- /dev/null +++ b/tests/spec/ParserSpec.php @@ -0,0 +1,187 @@ + + * @mixin Parser + */ +class ParserSpec extends ObjectBehavior +{ + function it_is_initializable() + { + $this->shouldHaveType('Madkom\NginxConfigurator\Parser'); + } + + /** + * @throws ParseFailureException + */ + function it_can_parse_directive() + { + /** @var RootNode $root */ + $root = $this->parse(<<shouldReturnAnInstanceOf(RootNode::class); + + $directives = $root->search(function (Node $node) { + return $node; + })->getWrappedObject(); + /** @var Directive $directive */ + foreach ($directives as $directive) { + break; + } + Assert::assertEquals($directive->getName(), 'internal'); + Assert::assertInstanceOf(Traversable::class, $directive->getParams()); + } + + function it_can_parse_multiple_directives_with_params() + { + $root = $this->parse(<<shouldReturnAnInstanceOf(RootNode::class); + + $directives = $root->search(function (Node $node) { + return $node; + })->getWrappedObject(); + /** @var Directive $directive */ + foreach ($directives as $index => $directive) { + switch ($index) { + case 0: + Assert::assertEquals($directive->getName(), 'internal'); + Assert::assertInstanceOf(Traversable::class, $directive->getParams()); + break; + case 1: + Assert::assertEquals($directive->getName(), 'sendfile'); + Assert::assertInstanceOf(Traversable::class, $directive->getParams()); + break; + case 2: + Assert::assertEquals($directive->getName(), 'set'); + Assert::assertInstanceOf(Traversable::class, $directive->getParams()); + break; + } + } + } + + function it_can_parse_multiple_directives_with_params_in_context() + { + $root = $this->parse(<<shouldReturnAnInstanceOf(RootNode::class); + + $contexts = $root->search(function (Node $node) { + return $node; + })->getWrappedObject(); + /** @var Context $context */ + foreach ($contexts as $index => $context) { + switch ($index) { + case 0: + Assert::assertInstanceOf(Server::class, $context); + /** @var Directive $directive */ + foreach ($context as $index => $directive) { + switch ($index) { + case 0: + Assert::assertEquals('internal', $directive->getName()); + break; + case 1: + Assert::assertEquals('sendfile', $directive->getName()); + break; + case 2: + Assert::assertEquals('set', $directive->getName()); + } + } + break; + } + } + } + + function it_can_parse_multiple_directives_with_params_in_multiple_contexts() + { + $root = $this->parse(<<shouldReturnAnInstanceOf(RootNode::class); + + $contexts = $root->search(function (Node $node) { + return $node; + })->getWrappedObject(); + /** @var Context $context */ + foreach ($contexts as $index => $context) { + switch ($index) { + case 0: + Assert::assertInstanceOf(Server::class, $context); + /** @var Directive $directive */ + foreach ($context as $index => $directive) { + switch ($index) { + case 0: + Assert::assertEquals('internal', $directive->getName()); + break; + case 1: + Assert::assertEquals('sendfile', $directive->getName()); + break; + case 2: + Assert::assertEquals('set', $directive->getName()); + break; + case 3: + Assert::assertInstanceOf(Location::class, $directive); + break; + } + } + break; + + case 1: + Assert::assertInstanceOf(Directive::class, $context); + Assert::assertEquals('sendfile', $context->getName()); + break; + + } + } + } +}