From 09139d2b14a742335472ea4d5f656caaf8ecaa0b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20Brzuchalski?=
Date: Fri, 10 Jun 2016 13:19:45 +0200
Subject: [PATCH] Initial commit
---
.coveralls.yml | 4 +
.gitignore | 21 ++
LICENSE | 21 ++
README.md | 233 ++++++++++++++++++
composer.json | 35 +++
examples/build.php | 40 +++
examples/default.conf | 25 ++
examples/parse.php | 54 ++++
phpcs.xml | 11 +
phpspec.yml | 5 +
phpunit.xml | 27 ++
src/Builder.php | 93 +++++++
src/Config/Events.php | 28 +++
src/Config/Http.php | 28 +++
src/Config/Location.php | 71 ++++++
src/Config/Server.php | 27 ++
src/Config/Upstream.php | 51 ++++
src/Exception/GrammarException.php | 20 ++
.../UnrecognizedContextException.php | 20 ++
src/Node/Context.php | 42 ++++
src/Node/Directive.php | 77 ++++++
src/Node/Literal.php | 24 ++
src/Node/Node.php | 122 +++++++++
src/Node/Param.php | 47 ++++
src/Node/RootNode.php | 28 +++
src/Parser.php | 220 +++++++++++++++++
tests/coverage/.gitkeep | 0
tests/spec/ParserSpec.php | 187 ++++++++++++++
28 files changed, 1561 insertions(+)
create mode 100644 .coveralls.yml
create mode 100644 .gitignore
create mode 100644 LICENSE
create mode 100644 README.md
create mode 100644 composer.json
create mode 100644 examples/build.php
create mode 100644 examples/default.conf
create mode 100644 examples/parse.php
create mode 100644 phpcs.xml
create mode 100644 phpspec.yml
create mode 100644 phpunit.xml
create mode 100644 src/Builder.php
create mode 100644 src/Config/Events.php
create mode 100644 src/Config/Http.php
create mode 100644 src/Config/Location.php
create mode 100644 src/Config/Server.php
create mode 100644 src/Config/Upstream.php
create mode 100644 src/Exception/GrammarException.php
create mode 100644 src/Exception/UnrecognizedContextException.php
create mode 100644 src/Node/Context.php
create mode 100644 src/Node/Directive.php
create mode 100644 src/Node/Literal.php
create mode 100644 src/Node/Node.php
create mode 100644 src/Node/Param.php
create mode 100644 src/Node/RootNode.php
create mode 100644 src/Parser.php
create mode 100644 tests/coverage/.gitkeep
create mode 100644 tests/spec/ParserSpec.php
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
+
+
+[](https://travis-ci.org/madkom/nginx-configurator)
+[](https://packagist.org/packages/madkom/nginx-configurator)
+[](https://packagist.org/packages/madkom/nginx-configurator)
+[](https://packagist.org/packages/madkom/nginx-configurator)
+[](https://coveralls.io/github/madkom/nginx-configurator?branch=master)
+[](https://codeclimate.com/github/madkom/nginx-configurator)
+[](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;
+
+ }
+ }
+ }
+}