diff --git a/README.md b/README.md index f2934e0..aef2a0a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ NGINX Configurator ================== -PHP Library for NGINX configuration parser/generator +PHP Library for NGINX configuration generator ![PHP 8.1](https://img.shields.io/badge/PHP-8.1-8C9CB6.svg?style=flat) @@ -27,60 +27,6 @@ This library requires *PHP* in `>=8.1` version. ## Usage -Parsing configuration string: - -```php -use Envoyr\NginxConfigurator\Builder; -use Envoyr\NginxConfigurator\Config\Server; -use Envoyr\NginxConfigurator\Parser; - -require 'vendor/autoload.php'; - -$config = <<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->append($defaultServer); - } -} -``` - Generating configuration string: ```php @@ -167,37 +113,6 @@ server { } ``` -There are also methods to read and dump file: - -```php -use Envoyr\NginxConfigurator\Builder; -use Envoyr\NginxConfigurator\Config\Location; -use Envoyr\NginxConfigurator\Config\Server; -use Envoyr\NginxConfigurator\Node\Directive; -use Envoyr\NginxConfigurator\Node\Literal; -use Envoyr\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->append($server); - } -} - -$builder->dumpFile('generated.conf'); -``` - ## TODO * [ ] Implement comments parsing diff --git a/composer.json b/composer.json index 512f1f1..fb52c91 100644 --- a/composer.json +++ b/composer.json @@ -3,10 +3,7 @@ "license": "MIT", "homepage": "https://envoyr.com/", "minimum-stability": "dev", - "require": { - "envoyr/collection": "dev-main", - "envoyr/loco": "dev-main" - }, + "require": {}, "require-dev": {}, "autoload": { "psr-4": { diff --git a/examples/build.php b/examples/build.php index 6f4701a..4073168 100644 --- a/examples/build.php +++ b/examples/build.php @@ -9,34 +9,22 @@ use Envoyr\NginxConfigurator\Node\Param; require __DIR__ . '/../vendor/autoload.php'; +$uuid = 'uuid'; + $factory = new Factory(); $builder = new Builder(); +$builder->append(new Directive('proxy_cache_path', [new Param("/data/nginx/cache/{$uuid}"), new Param("keys_zone={$uuid}:10m")])); $server = $builder->append($factory->createServer(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 Directive('error_log', [new Param("/data/nginx/log/{$uuid}.log"), new Param('debug')])); +$server->append(new Directive('proxy_cache', [new Param($uuid)])); +$server->append(new Location(new Param('/'), null, [ + new Directive('error_page', [new Param('401')]), + new Directive('host', [new Param('example.com')]), + new Directive('proxy_pass', [new Param('https://example.com')]), ])); -$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')]), +$server->append(new Location(new Param('/.well-known/acme-challenge'), new Param('^~'), [ + new Directive('proxy_pass', [new Param('https://le.cdn.gd')]) ])); print($builder->dump()); \ No newline at end of file diff --git a/examples/parse.php b/examples/parse.php deleted file mode 100644 index b0e5a3f..0000000 --- a/examples/parse.php +++ /dev/null @@ -1,54 +0,0 @@ -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->append($defaultServer); - } -} diff --git a/src/Builder.php b/src/Builder.php index 10a3629..0464437 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -8,7 +8,7 @@ namespace Envoyr\NginxConfigurator; use Countable; -use Envoyr\Collection\CustomTypedCollection; +use Envoyr\NginxConfigurator\Collection\CustomTypedCollection; use Envoyr\NginxConfigurator\Node\Node; use Envoyr\NginxConfigurator\Node\RootNode; use Traversable; diff --git a/src/Collection/Collection.php b/src/Collection/Collection.php new file mode 100644 index 0000000..f0701e7 --- /dev/null +++ b/src/Collection/Collection.php @@ -0,0 +1,154 @@ + + */ +class Collection implements Countable, IteratorAggregate, Serializable +{ + /** + * @var array + */ + protected $elements = []; + + /** + * Collection constructor. + * @param array $elements + */ + public function __construct(array $elements = []) + { + foreach ($elements as $element) { + $this->add($element); + } + } + + /** + * Add the element to collection + * @param $element + * @return bool + */ + public function add($element) : bool + { + $this->elements[] = $element; + + return $this->contains($element); + } + + /** + * Removes element and checks if collection not contains it anymore + * @param $element + * @return bool + */ + public function remove($element) : bool + { + if (!$this->contains($element)) { + return false; + } + $index = array_search($element, $this->elements, true); + unset($this->elements[$index]); + + return !$this->contains($element); + } + + /** + * Return element existence in collection test result + * @param $element + * @return bool + */ + public function contains($element) : bool + { + return in_array($element, $this->elements, true); + } + + /** + * Iterates through all elements to exam existence by callback + * @param callable $checker + * @return bool + */ + public function exists(callable $checker) + { + foreach ($this->elements as $element) { + if ($checker($element)) { + return true; + } + } + + return false; + } + + /** + * Iterates through all elements to exam existence by callback + * @param callable $checker + * @return bool + */ + public function filter(callable $checker) + { + $result = new static(); + foreach ($this->elements as $element) { + if ($checker($element)) { + $result->add($element); + } + } + + return $result; + } + + /** + * String representation of object + * @link http://php.net/manual/en/serializable.serialize.php + * @return string the string representation of the object or null + * @since 5.1.0 + */ + public function serialize() + { + return serialize($this->elements); + } + + /** + * Constructs the object + * @link http://php.net/manual/en/serializable.unserialize.php + * @param string $serialized

+ * The string representation of the object. + *

+ * @return void + * @since 5.1.0 + */ + public function unserialize($serialized) + { + $this->elements = unserialize($serialized); + } + + /** + * 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->elements); + } + + /** + * 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 new ArrayIterator($this->elements); + } +} diff --git a/src/Collection/ComparableDistinctTypedCollection.php b/src/Collection/ComparableDistinctTypedCollection.php new file mode 100644 index 0000000..c2265b2 --- /dev/null +++ b/src/Collection/ComparableDistinctTypedCollection.php @@ -0,0 +1,53 @@ + + */ +abstract class ComparableDistinctTypedCollection extends CustomTypedCollection +{ + /** + * Checks two elements are equal + * @param $left + * @param $right + * @return bool + */ + abstract public function compareTo($left, $right) : bool; + + /** + * @inheritDoc + */ + public function add($element) : bool + { + if ($this->contains($element)) { + throw new RuntimeException("Given element already exists in collection"); + } + + return parent::add($element); + } + + + /** + * @inheritDoc + */ + public function contains($element) : bool + { + if (!$this->isElementValid($element)) { + throw new UnexpectedValueException( + "Unexpected element type, expecting: {$this->getType()}, given: " . get_class($element) + ); + } + foreach ($this->elements as $current) { + if ($this->compareTo($current, $element)) { + return true; + } + } + + return false; + } +} diff --git a/src/Collection/CustomDistinctCollection.php b/src/Collection/CustomDistinctCollection.php new file mode 100644 index 0000000..7bcf324 --- /dev/null +++ b/src/Collection/CustomDistinctCollection.php @@ -0,0 +1,51 @@ + + */ +abstract class CustomDistinctCollection extends CustomTypedCollection +{ + /** + * @return string + */ + abstract protected function getMethod() : string; + + /** + * @inheritDoc + */ + public function add($element) : bool + { + if ($this->contains($element)) { + throw new RuntimeException("Given element already exists in collection"); + } + + return parent::add($element); + } + + + /** + * @inheritDoc + */ + public function contains($element) : bool + { + if (!$this->isElementValid($element)) { + throw new UnexpectedValueException( + "Unexpected element type, expecting: {$this->getType()}, given: " . get_class($element) + ); + } + $distinct = $element->{$this->getMethod()}(); + foreach ($this->elements as $current) { + if ($current->{$this->getMethod()}() == $distinct) { + return true; + } + } + + return false; + } +} diff --git a/src/Collection/CustomTypedCollection.php b/src/Collection/CustomTypedCollection.php new file mode 100644 index 0000000..0fcba07 --- /dev/null +++ b/src/Collection/CustomTypedCollection.php @@ -0,0 +1,69 @@ + + */ +abstract class CustomTypedCollection extends Collection +{ + /** + * Retrieves collection type + * @return string + */ + abstract protected function getType() : string; + + /** + * AbstractTypedCollection constructor. + * @param array $elements + */ + public function __construct(array $elements = []) + { + if (!class_exists($this->getType())) { + throw new InvalidArgumentException("Expected type should be accessible class name, given: {$this->getType()}"); + } + parent::__construct($elements); + } + + /** + * @inheritDoc + */ + public function add($element) : bool + { + if (!$this->isElementValid($element)) { + throw new UnexpectedValueException( + "Unexpected element type, expecting: {$this->getType()}, given: " . get_class($element) + ); + } + + return parent::add($element); + } + + /** + * @inheritDoc + */ + public function remove($element) : bool + { + if (!$this->isElementValid($element)) { + throw new UnexpectedValueException( + "Unexpected element type, expecting: {$this->getType()}, given: " . get_class($element) + ); + } + + return parent::remove($element); + } + + /** + * Check is element valid object type + * @param $element + * @return bool + */ + protected function isElementValid($element) : bool + { + return is_a($element, $this->getType()) || is_subclass_of($element, $this->getType()); + } +} diff --git a/src/Collection/DistinctCollection.php b/src/Collection/DistinctCollection.php new file mode 100644 index 0000000..3ee3336 --- /dev/null +++ b/src/Collection/DistinctCollection.php @@ -0,0 +1,57 @@ + + */ +class DistinctCollection extends CustomDistinctCollection +{ + /** + * @var string Collection generic type + */ + protected $type; + /** + * @var string Generic type distinguish method name + */ + private $method; + + /** + * UniqueGenericCollection constructor. + * @param string $type + * @param string $method + * @param array $elements + */ + public function __construct(string $type, string $method, array $elements = []) + { + if (!method_exists($type, $method) && is_callable($type, $method)) { + throw new InvalidArgumentException( + "Non-existent distinct method name in class {$type}, given: {$method}" + ); + } + $this->type = $type; + $this->method = $method; + parent::__construct($elements); + } + + /** + * @return string + */ + protected function getType() : string + { + return $this->type; + } + + /** + * @inheritDoc + */ + protected function getMethod() : string + { + return $this->method; + } +} diff --git a/src/Collection/TypedCollection.php b/src/Collection/TypedCollection.php new file mode 100644 index 0000000..28c5e4b --- /dev/null +++ b/src/Collection/TypedCollection.php @@ -0,0 +1,34 @@ + + */ +class TypedCollection extends CustomTypedCollection +{ + /** + * @var string Collection generic type + */ + protected $type; + + /** + * GenericCollection constructor. + * @param string $type + * @param array $elements + */ + public function __construct(string $type, array $elements = []) + { + $this->type = $type; + parent::__construct($elements); + } + + /** + * @return string + */ + protected function getType() : string + { + return $this->type; + } +} diff --git a/src/Node/Directive.php b/src/Node/Directive.php index bbe5228..39adc88 100644 --- a/src/Node/Directive.php +++ b/src/Node/Directive.php @@ -7,7 +7,7 @@ */ namespace Envoyr\NginxConfigurator\Node; -use Envoyr\Collection\CustomTypedCollection; +use Envoyr\NginxConfigurator\Collection\CustomTypedCollection; use Traversable; /** diff --git a/src/Node/Node.php b/src/Node/Node.php index ad1a9d4..6cd7729 100644 --- a/src/Node/Node.php +++ b/src/Node/Node.php @@ -9,7 +9,7 @@ namespace Envoyr\NginxConfigurator\Node; use Countable; use IteratorAggregate; -use Envoyr\Collection\CustomTypedCollection; +use Envoyr\NginxConfigurator\Collection\CustomTypedCollection; use Traversable; /** diff --git a/src/Parser.php b/src/Parser.php deleted file mode 100644 index 972ec09..0000000 --- a/src/Parser.php +++ /dev/null @@ -1,220 +0,0 @@ - - */ -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, $params, $open, $space1, $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); - } -}