v2.6.0: Content backup/git versioning, plugin type system, docs update

New features:
- ContentBackup class with ZIP backup/restore and git versioning
- Admin backup & restore page (content-backup.twig) with git init/commit/log/restore
- Plugin type system: system (blue) vs content (green) with visual badges
- PluginAPIInterface + AdminPluginAPI for plugin architecture
- Essential plugin flag (cannot edit/deactivate/delete)

Improvements:
- Consolidated enabled_plugins config (removed plugins.enabled)
- Removed Analytics/Logging toggles from admin config page
- Fixed Dashboard plugin Twig comments rendered as text
- Updated 20 guide files (NL+EN): configuratie, plugins, plugin-development,
  core-classes, theme-json, layouts, scss-styling, admin-beheerder, nieuw-thema, architectuur
- Improved accessibility test script (grep -E, min/max checks)

Cleanup:
- Removed unused classes: ARIAComponents, AccessibilityManager, ContentSecurityPolicy, etc.
- Removed vendor packages: mustache/mustache, php-mqtt/client
- Removed old templates: logs.twig, statistics.twig (now plugins)
- Moved language files to language/ directory

Tests:
- Pentest: 30/30 passed, 0 vulnerabilities
- WCAG 2.1 AA: 25/25 passed, 100% compliance
This commit is contained in:
2026-08-15 19:21:04 +02:00
parent 3a55ea4db6
commit 20adea7544
208 changed files with 3763 additions and 22688 deletions
-2
View File
@@ -16,10 +16,8 @@ return array(
'Psr\\Log\\' => array($vendorDir . '/psr/log/src'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
'Psr\\EventDispatcher\\' => array($vendorDir . '/psr/event-dispatcher/src'),
'PhpMqtt\\Client\\' => array($vendorDir . '/php-mqtt/client/src'),
'Nette\\' => array($vendorDir . '/nette/schema/src', $vendorDir . '/nette/utils/src'),
'MyCLabs\\Enum\\' => array($vendorDir . '/myclabs/php-enum/src'),
'Mustache\\' => array($vendorDir . '/mustache/mustache/src'),
'MaxMind\\WebService\\' => array($vendorDir . '/maxmind/web-service-common/src/WebService'),
'MaxMind\\Exception\\' => array($vendorDir . '/maxmind/web-service-common/src/Exception'),
'MaxMind\\Db\\' => array($vendorDir . '/maxmind-db/reader/src/MaxMind/Db'),
-47
View File
@@ -1,47 +0,0 @@
name: Tests
on:
push:
pull_request:
schedule:
- cron: '0 0 * * *'
jobs:
tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php:
- 5.6
- 7.0
- 7.1
- 7.2
- 7.3
- 7.4
- 8.0
- 8.1
- 8.2
- 8.3
- 8.4
name: PHP ${{ matrix.php }}
steps:
- name: Check out code
uses: actions/checkout@v4
with:
submodules: true
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: none
- name: Install dependencies
run: composer install --prefer-dist --no-interaction --no-progress
- name: Run tests
run: vendor/bin/phpunit
-20
View File
@@ -1,20 +0,0 @@
<?php
use PhpCsFixer\Config;
$config = new Config();
$config->setRules([
'@Symfony' => true,
'binary_operator_spaces' => false,
'concat_space' => ['spacing' => 'one'],
'increment_style' => false,
'single_line_throw' => false,
'yoda_style' => false,
]);
$finder = $config->getFinder()
->in('src')
->in('test');
return $config;
-21
View File
@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2010-2025 Justin Hileman
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.
-94
View File
@@ -1,94 +0,0 @@
# Mustache.php
A [Mustache][mustache] implementation in PHP.
[![Package version](http://img.shields.io/packagist/v/mustache/mustache.svg?style=flat-square)][packagist]
[![Monthly downloads](http://img.shields.io/packagist/dm/mustache/mustache.svg?style=flat-square)][packagist]
## Installation
```
composer require mustache/mustache
```
## Usage
A quick example:
```php
<?php
$m = new \Mustache\Engine(['entity_flags' => ENT_QUOTES]);
echo $m->render('Hello {{planet}}', ['planet' => 'World!']); // "Hello World!"
```
And a more in-depth example -- this is the canonical Mustache template:
```html+jinja
Hello {{name}}
You have just won {{value}} dollars!
{{#in_ca}}
Well, {{taxed_value}} dollars, after taxes.
{{/in_ca}}
```
Create a view "context" object -- which could also be an associative array, but those don't do functions quite as well:
```php
<?php
class Chris {
public $name = "Chris";
public $value = 10000;
public function taxed_value() {
return $this->value - ($this->value * 0.4);
}
public $in_ca = true;
}
```
And render it:
```php
<?php
$m = new \Mustache\Engine(['entity_flags' => ENT_QUOTES]);
$chris = new \Chris;
echo $m->render($template, $chris);
```
*Note:* we recommend using `ENT_QUOTES` as a default of [entity_flags][entity_flags] to decrease the chance of Cross-site scripting vulnerability.
## And That's Not All!
Read [the Mustache.php documentation][docs] for more information.
## Upgrading from v2.x
_Mustache.php v3.x drops support for PHP 5.25.5_, but is otherwise backwards compatible with v2.x.
To ease the transition, previous behavior can be preserved via configuration:
- The `strict_callables` config option now defaults to `true`. Lambda sections should use closures or callable objects. To continue supporting array-style callables for lambda sections (e.g. `[$this, 'foo']`), set `strict_callables` to `false`.
- [A context shadowing bug from v2.x has been fixed](https://github.com/bobthecow/mustache.php/commit/66ecb327ce15b9efa0cfcb7026fdc62c6659b27f), but if you depend on the previous buggy behavior you can preserve it via the `buggy_property_shadowing` config option.
- By default the return value of higher-order sections that are rendered via the lambda helper will no longer be double-rendered. To preserve the previous behavior, set `double_render_lambdas` to `true`. _This is not recommended._
In order to maintain a wide PHP version support range, there are minor changes to a few interfaces, which you might need to handle if you extend Mustache (see [c0453be](https://github.com/bobthecow/mustache.php/commit/c0453be5c09e7d988b396982e29218fcb25b7304)).
## See Also
- [mustache(5)][manpage] man page.
- [Readme for the Ruby Mustache implementation][ruby].
[mustache]: https://mustache.github.io/
[packagist]: https://packagist.org/packages/mustache/mustache
[entity_flags]: https://github.com/bobthecow/mustache.php/wiki#entity_flags
[docs]: https://github.com/bobthecow/mustache.php/wiki/Home
[manpage]: https://mustache.github.io/mustache.5.html
[ruby]: https://github.com/mustache/mustache/blob/master/README.md
-38
View File
@@ -1,38 +0,0 @@
{
"name": "mustache/mustache",
"description": "A Mustache implementation in PHP.",
"keywords": [
"templating",
"mustache"
],
"homepage": "https://github.com/bobthecow/mustache.php",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Justin Hileman",
"email": "justin@justinhileman.info",
"homepage": "http://justinhileman.com"
}
],
"require": {
"php": ">=5.6"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.19.3",
"yoast/phpunit-polyfills": "^2.0"
},
"autoload": {
"psr-4": {
"Mustache\\": "src/"
},
"classmap": [
"src/compat.php"
]
},
"autoload-dev": {
"psr-4": {
"Mustache\\Test\\": "test/"
}
}
}
-46
View File
@@ -1,46 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
use Psr\Log\LoggerInterface;
/**
* Mustache Cache interface.
*
* Interface for caching and loading Template classes generated by the Compiler.
*/
interface Cache
{
/**
* Load a compiled Template class from cache.
*
* @param string $key
*
* @return bool indicates successfully class load
*/
public function load($key);
/**
* Cache and load a compiled Template class.
*
* @param string $key
* @param string $value
*/
public function cache($key, $value);
/**
* Set a logger instance.
*
* @param Logger|LoggerInterface $logger
*/
public function setLogger($logger = null);
}
-68
View File
@@ -1,68 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Cache;
use Mustache\Cache;
use Mustache\Exception\InvalidArgumentException;
use Mustache\Logger;
use Psr\Log\LoggerInterface;
/**
* Abstract Mustache Cache class.
*
* Provides logging support to child implementations.
*
* @abstract
*/
abstract class AbstractCache implements Cache
{
private $logger = null;
/**
* Get the current logger instance.
*
* @return Logger|LoggerInterface
*/
public function getLogger()
{
return $this->logger;
}
/**
* Set a logger instance.
*
* @param Logger|LoggerInterface $logger
*/
public function setLogger($logger = null)
{
// n.b. this uses `is_a` to prevent a dependency on Psr\Log
if ($logger !== null && !$logger instanceof Logger && !is_a($logger, 'Psr\\Log\\LoggerInterface')) {
throw new InvalidArgumentException('Expected an instance of Mustache\\Logger or Psr\\Log\\LoggerInterface.');
}
$this->logger = $logger;
}
/**
* Add a log record if logging is enabled.
*
* @param string $level The logging level
* @param string $message The log message
* @param array $context The log context
*/
protected function log($level, $message, array $context = [])
{
if (isset($this->logger)) {
$this->logger->log($level, $message, $context);
}
}
}
-166
View File
@@ -1,166 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Cache;
use Mustache\Exception\RuntimeException;
use Mustache\Logger;
/**
* Mustache Cache filesystem implementation.
*
* A FilesystemCache instance caches Mustache Template classes from the filesystem by name:
*
* $cache = new FilesystemCache(__DIR__.'/cache');
* $cache->cache($className, $compiledSource);
*
* The FilesystemCache benefits from any opcode caching that may be setup in your environment. So do that, k?
*/
class FilesystemCache extends AbstractCache
{
private $baseDir;
private $fileMode;
/**
* Filesystem cache constructor.
*
* @param string $baseDir Directory for compiled templates
* @param int $fileMode Override default permissions for cache files. Defaults to using the system-defined umask
*/
public function __construct($baseDir, $fileMode = null)
{
$this->baseDir = $baseDir;
$this->fileMode = $fileMode;
}
/**
* Load the class from cache using `require_once`.
*
* @param string $key
*
* @return bool
*/
public function load($key)
{
$fileName = $this->getCacheFilename($key);
if (!is_file($fileName)) {
return false;
}
require_once $fileName;
return true;
}
/**
* Cache and load the compiled class.
*
* @param string $key
* @param string $value
*/
public function cache($key, $value)
{
$fileName = $this->getCacheFilename($key);
$this->log(
Logger::DEBUG,
'Writing to template cache: "{fileName}"',
['fileName' => $fileName]
);
$this->writeFile($fileName, $value);
$this->load($key);
}
/**
* Build the cache filename.
* Subclasses should override for custom cache directory structures.
*
* @param string $name
*
* @return string
*/
protected function getCacheFilename($name)
{
return sprintf('%s/%s.php', $this->baseDir, $name);
}
/**
* Create cache directory.
*
* @throws RuntimeException If unable to create directory
*
* @param string $fileName
*
* @return string
*/
private function buildDirectoryForFilename($fileName)
{
$dirName = dirname($fileName);
if (!is_dir($dirName)) {
$this->log(
Logger::INFO,
'Creating Mustache template cache directory: "{dirName}"',
['dirName' => $dirName]
);
@mkdir($dirName, 0777, true);
// @codeCoverageIgnoreStart
if (!is_dir($dirName)) {
throw new RuntimeException(sprintf('Failed to create cache directory "%s".', $dirName));
}
// @codeCoverageIgnoreEnd
}
return $dirName;
}
/**
* Write cache file.
*
* @throws RuntimeException If unable to write file
*
* @param string $fileName
* @param string $value
*/
private function writeFile($fileName, $value)
{
$dirName = $this->buildDirectoryForFilename($fileName);
$this->log(
Logger::DEBUG,
'Caching compiled template to "{fileName}"',
['fileName' => $fileName]
);
$tempFile = tempnam($dirName, basename($fileName));
if (false !== @file_put_contents($tempFile, $value)) {
if (@rename($tempFile, $fileName)) {
$mode = isset($this->fileMode) ? $this->fileMode : (0666 & ~umask());
@chmod($fileName, $mode);
return;
}
// @codeCoverageIgnoreStart
$this->log(
Logger::ERROR,
'Unable to rename Mustache temp cache file: "{tempName}" -> "{fileName}"',
['tempName' => $tempFile, 'fileName' => $fileName]
);
// @codeCoverageIgnoreEnd
}
// @codeCoverageIgnoreStart
throw new RuntimeException(sprintf('Failed to write cache file "%s".', $fileName));
// @codeCoverageIgnoreEnd
}
}
-51
View File
@@ -1,51 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Cache;
use Mustache\Logger;
/**
* Mustache Cache in-memory implementation.
*
* The in-memory cache is used for uncached lambda section templates. It's also useful during development, but is not
* recommended for production use.
*/
class NoopCache extends AbstractCache
{
/**
* Loads nothing. Move along.
*
* @param string $key
*
* @return bool
*/
public function load($key)
{
return false;
}
/**
* Loads the compiled Mustache Template class without caching.
*
* @param string $key
* @param string $value
*/
public function cache($key, $value)
{
$this->log(
Logger::WARNING,
'Template cache disabled, evaluating "{className}" class at runtime',
['className' => $key]
);
eval('?>' . $value);
}
}
-807
View File
@@ -1,807 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
use Mustache\Exception\InvalidArgumentException;
use Mustache\Exception\SyntaxException;
/**
* Mustache Compiler class.
*
* This class is responsible for turning a Mustache token parse tree into normal PHP source code.
*/
class Compiler
{
private $pragmas;
private $defaultPragmas = [];
private $sections;
private $blocks;
private $source;
private $indentNextLine;
private $customEscape;
private $entityFlags;
private $charset;
private $strictCallables;
// Optional Mustache specs
private $lambdas = true;
/**
* Compile a Mustache token parse tree into PHP source code.
*
* @throws InvalidArgumentException if the FILTERS pragma is set but lambdas are not enabled
*
* @param string $source Mustache Template source code
* @param array $tree Parse tree of Mustache tokens
* @param string $name Mustache Template class name
* @param bool $customEscape (default: false)
* @param string $charset (default: 'UTF-8')
* @param bool $strictCallables (default: false)
* @param int $entityFlags (default: ENT_COMPAT)
*
* @return string Generated PHP source code
*/
public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT)
{
$this->pragmas = $this->defaultPragmas;
$this->sections = [];
$this->blocks = [];
$this->source = $source;
$this->indentNextLine = true;
$this->customEscape = $customEscape;
$this->entityFlags = $entityFlags;
$this->charset = $charset;
$this->strictCallables = $strictCallables;
$code = $this->writeCode($tree, $name);
if (isset($this->pragmas[Engine::PRAGMA_FILTERS]) && !$this->lambdas) {
throw new InvalidArgumentException('The FILTERS pragma requires lambda support');
}
return $code;
}
/**
* Disable optional Mustache specs.
*
* @internal Users should set options in Mustache\Engine, not here :)
*
* @param bool[] $options
*/
public function setOptions(array $options)
{
if (isset($options['lambdas'])) {
$this->lambdas = $options['lambdas'] !== false;
}
}
/**
* Enable pragmas across all templates, regardless of the presence of pragma
* tags in the individual templates.
*
* @internal Users should set global pragmas in \Mustache\Engine, not here :)
*
* @param string[] $pragmas
*/
public function setPragmas(array $pragmas)
{
$this->pragmas = [];
foreach ($pragmas as $pragma) {
$this->pragmas[$pragma] = true;
}
$this->defaultPragmas = $this->pragmas;
}
/**
* Helper function for walking the Mustache token parse tree.
*
* @throws SyntaxException upon encountering unknown token types
*
* @param array $tree Parse tree of Mustache tokens
* @param int $level (default: 0)
*
* @return string Generated PHP source code
*/
private function walk(array $tree, $level = 0)
{
$code = '';
$level++;
foreach ($tree as $node) {
switch ($node[Tokenizer::TYPE]) {
case Tokenizer::T_PRAGMA:
$this->pragmas[$node[Tokenizer::NAME]] = true;
break;
case Tokenizer::T_SECTION:
$code .= $this->section(
$node[Tokenizer::NODES],
$node[Tokenizer::NAME],
isset($node[Tokenizer::FILTERS]) ? $node[Tokenizer::FILTERS] : [],
$node[Tokenizer::INDEX],
$node[Tokenizer::END],
$node[Tokenizer::OTAG],
$node[Tokenizer::CTAG],
$level
);
break;
case Tokenizer::T_INVERTED:
$code .= $this->invertedSection(
$node[Tokenizer::NODES],
$node[Tokenizer::NAME],
isset($node[Tokenizer::FILTERS]) ? $node[Tokenizer::FILTERS] : [],
$level
);
break;
case Tokenizer::T_PARTIAL:
$code .= $this->partial(
$node[Tokenizer::NAME],
isset($node[Tokenizer::DYNAMIC]) ? $node[Tokenizer::DYNAMIC] : false,
isset($node[Tokenizer::INDENT]) ? $node[Tokenizer::INDENT] : '',
$level
);
break;
case Tokenizer::T_PARENT:
$code .= $this->parent(
$node[Tokenizer::NAME],
isset($node[Tokenizer::DYNAMIC]) ? $node[Tokenizer::DYNAMIC] : false,
isset($node[Tokenizer::INDENT]) ? $node[Tokenizer::INDENT] : '',
$node[Tokenizer::NODES],
$level
);
break;
case Tokenizer::T_BLOCK_ARG:
$code .= $this->blockArg(
$node[Tokenizer::NODES],
$node[Tokenizer::NAME],
$node[Tokenizer::INDEX],
$node[Tokenizer::END],
$node[Tokenizer::OTAG],
$node[Tokenizer::CTAG],
$level
);
break;
case Tokenizer::T_BLOCK_VAR:
$code .= $this->blockVar(
$node[Tokenizer::NODES],
$node[Tokenizer::NAME],
$node[Tokenizer::INDEX],
$node[Tokenizer::END],
$node[Tokenizer::OTAG],
$node[Tokenizer::CTAG],
$level
);
break;
case Tokenizer::T_COMMENT:
break;
case Tokenizer::T_ESCAPED:
case Tokenizer::T_UNESCAPED:
case Tokenizer::T_UNESCAPED_2:
$code .= $this->variable(
$node[Tokenizer::NAME],
isset($node[Tokenizer::FILTERS]) ? $node[Tokenizer::FILTERS] : [],
$node[Tokenizer::TYPE] === Tokenizer::T_ESCAPED,
$level
);
break;
case Tokenizer::T_TEXT:
$code .= $this->text($node[Tokenizer::VALUE], $level);
break;
default:
throw new SyntaxException(sprintf('Unknown token type: %s', $node[Tokenizer::TYPE]), $node);
}
}
return $code;
}
const KLASS = '<?php
class %s extends \\Mustache\\Template
{
private $lambdaHelper;%s%s
public function renderInternal(\\Mustache\\Context $context, $indent = \'\')
{
$this->lambdaHelper = new \\Mustache\\LambdaHelper($this->mustache, $context);
$buffer = \'\';
%s
return $buffer;
}
%s
%s
}';
const KLASS_NO_LAMBDAS = '<?php
class %s extends \\Mustache\\Template
{%s%s
public function renderInternal(\\Mustache\\Context $context, $indent = \'\')
{
$buffer = \'\';
%s
return $buffer;
}
}';
const STRICT_CALLABLE = 'protected $strictCallables = true;';
const NO_LAMBDAS = 'protected $lambdas = false;';
/**
* Generate Mustache Template class PHP source.
*
* @param array $tree Parse tree of Mustache tokens
* @param string $name Mustache Template class name
*
* @return string Generated PHP source code
*/
private function writeCode(array $tree, $name)
{
$code = $this->walk($tree);
$sections = implode("\n", $this->sections);
$blocks = implode("\n", $this->blocks);
$klass = empty($this->sections) && empty($this->blocks) ? self::KLASS_NO_LAMBDAS : self::KLASS;
$callable = $this->strictCallables ? $this->prepare(self::STRICT_CALLABLE) : '';
$lambda = $this->lambdas ? '' : $this->prepare(self::NO_LAMBDAS);
return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $lambda, $code, $sections, $blocks);
}
const BLOCK_VAR = '
$blockFunction = $context->findInBlock(%s);
if (is_callable($blockFunction)) {
$buffer .= call_user_func($blockFunction, $context);
%s}
';
const BLOCK_VAR_ELSE = '} else {%s';
/**
* Generate Mustache Template inheritance block variable PHP source.
*
* @param array $nodes Array of child tokens
* @param string $id Section name
* @param int $start Section start offset
* @param int $end Section end offset
* @param string $otag Current Mustache opening tag
* @param string $ctag Current Mustache closing tag
* @param int $level
*
* @return string Generated PHP source code
*/
private function blockVar(array $nodes, $id, $start, $end, $otag, $ctag, $level)
{
$id = var_export($id, true);
$else = $this->walk($nodes, $level);
if ($else !== '') {
$else = sprintf($this->prepare(self::BLOCK_VAR_ELSE, $level + 1, false, true), $else);
}
return sprintf($this->prepare(self::BLOCK_VAR, $level), $id, $else);
}
const BLOCK_ARG = '%s => [$this, \'block%s\'],';
/**
* Generate Mustache Template inheritance block argument PHP source.
*
* @param array $nodes Array of child tokens
* @param string $id Section name
* @param int $start Section start offset
* @param int $end Section end offset
* @param string $otag Current Mustache opening tag
* @param string $ctag Current Mustache closing tag
* @param int $level
*
* @return string Generated PHP source code
*/
private function blockArg($nodes, $id, $start, $end, $otag, $ctag, $level)
{
$key = $this->block($nodes);
$id = var_export($id, true);
return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key);
}
const BLOCK_FUNCTION = '
public function block%s($context)
{
$indent = $buffer = \'\';%s
return $buffer;
}
';
/**
* Generate Mustache Template inheritance block function PHP source.
*
* @param array $nodes Array of child tokens
*
* @return string key of new block function
*/
private function block(array $nodes)
{
$code = $this->walk($nodes, 0);
$key = ucfirst(md5($code));
if (!isset($this->blocks[$key])) {
$this->blocks[$key] = sprintf($this->prepare(self::BLOCK_FUNCTION, 0), $key, $code);
}
return $key;
}
const SECTION_CALL = '
$value = $context->%s(%s%s);%s
$buffer .= $this->section%s($context, $indent, $value);
';
const SECTION = '
private function section%s(\\Mustache\\Context $context, $indent, $value)
{
$buffer = \'\';
if (%s) {
$source = %s;
$value = call_user_func($value, $source, %s);
if ($value instanceof \\Mustache\\RenderedString) {
return $value->getValue();
}
if (is_string($value)) {
if (strpos($value, \'{{\') === false) {
return $value;
}
return $this->mustache
->loadLambda($value%s)
->renderInternal($context);
}
}
if (!empty($value)) {
$values = $this->isIterable($value) ? $value : [$value];
foreach ($values as $value) {
$context->push($value);
%s
$context->pop();
}
}
return $buffer;
}
';
const SECTION_NO_LAMBDAS = '
private function section%s(\\Mustache\\Context $context, $indent, $value)
{
$buffer = \'\';
if (!empty($value)) {
$values = $this->isIterable($value) ? $value : [$value];
foreach ($values as $value) {
$context->push($value);
%s
$context->pop();
}
}
return $buffer;
}
';
/**
* Generate Mustache Template section PHP source.
*
* @param array $nodes Array of child tokens
* @param string $id Section name
* @param string[] $filters Array of filters
* @param int $start Section start offset
* @param int $end Section end offset
* @param string $otag Current Mustache opening tag
* @param string $ctag Current Mustache closing tag
* @param int $level
*
* @return string Generated section PHP source code
*/
private function section(array $nodes, $id, $filters, $start, $end, $otag, $ctag, $level)
{
$source = var_export(substr($this->source, $start, $end - $start), true);
$callable = $this->getCallable();
if ($otag !== '{{' || $ctag !== '}}') {
$delimTag = var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true);
$helper = sprintf('$this->lambdaHelper->withDelimiters(%s)', $delimTag);
$delims = ', ' . $delimTag;
} else {
$helper = '$this->lambdaHelper';
$delims = '';
}
$key = ucfirst(md5($delims . "\n" . $source));
if (!isset($this->sections[$key])) {
if ($this->lambdas) {
$this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $helper, $delims, $this->walk($nodes, 2));
} else {
$this->sections[$key] = sprintf($this->prepare(self::SECTION_NO_LAMBDAS), $key, $this->walk($nodes, 2));
}
}
$method = $this->getFindMethod($id);
$id = var_export($id, true);
$findArg = $this->getFindMethodArgs($method);
$filters = $this->getFilters($filters, $level);
return sprintf($this->prepare(self::SECTION_CALL, $level), $method, $id, $findArg, $filters, $key);
}
const INVERTED_SECTION = '
$value = $context->%s(%s%s);%s
if (empty($value)) {
%s
}
';
/**
* Generate Mustache Template inverted section PHP source.
*
* @param array $nodes Array of child tokens
* @param string $id Section name
* @param string[] $filters Array of filters
* @param int $level
*
* @return string Generated inverted section PHP source code
*/
private function invertedSection(array $nodes, $id, $filters, $level)
{
$method = $this->getFindMethod($id);
$id = var_export($id, true);
$findArg = $this->getFindMethodArgs($method);
$filters = $this->getFilters($filters, $level);
return sprintf($this->prepare(self::INVERTED_SECTION, $level), $method, $id, $findArg, $filters, $this->walk($nodes, $level));
}
const DYNAMIC_NAME = '$this->resolveValue($context->%s(%s%s), $context)';
/**
* Generate Mustache Template dynamic name resolution PHP source.
*
* @param string $id Tag name
* @param bool $dynamic True if the name is dynamic
*
* @return string Dynamic name resolution PHP source code
*/
private function resolveDynamicName($id, $dynamic)
{
if (!$dynamic) {
return var_export($id, true);
}
$method = $this->getFindMethod($id);
$id = ($method !== 'last') ? var_export($id, true) : '';
$findArg = $this->getFindMethodArgs($method);
// TODO: filters?
return sprintf(self::DYNAMIC_NAME, $method, $id, $findArg);
}
const PARTIAL_INDENT = ', $indent . %s';
const PARTIAL = '
if ($partial = $this->mustache->loadPartial(%s)) {
$buffer .= $partial->renderInternal($context%s);
}
';
/**
* Generate Mustache Template partial call PHP source.
*
* @param string $id Partial name
* @param bool $dynamic Partial name is dynamic
* @param string $indent Whitespace indent to apply to partial
* @param int $level
*
* @return string Generated partial call PHP source code
*/
private function partial($id, $dynamic, $indent, $level)
{
if ($indent !== '') {
$indentParam = sprintf(self::PARTIAL_INDENT, var_export($indent, true));
} else {
$indentParam = '';
}
return sprintf(
$this->prepare(self::PARTIAL, $level),
$this->resolveDynamicName($id, $dynamic),
$indentParam
);
}
const PARENT = '
if ($parent = $this->mustache->loadPartial(%s)) {
$context->pushBlockContext([%s
]);
$buffer .= $parent->renderInternal($context, $indent);
$context->popBlockContext();
}
';
const PARENT_NO_CONTEXT = '
if ($parent = $this->mustache->loadPartial(%s)) {
$buffer .= $parent->renderInternal($context, $indent);
}
';
/**
* Generate Mustache Template inheritance parent call PHP source.
*
* @param string $id Parent tag name
* @param bool $dynamic Tag name is dynamic
* @param string $indent Whitespace indent to apply to parent
* @param array $children Child nodes
* @param int $level
*
* @return string Generated PHP source code
*/
private function parent($id, $dynamic, $indent, array $children, $level)
{
$realChildren = array_filter($children, [self::class, 'onlyBlockArgs']);
$partialName = $this->resolveDynamicName($id, $dynamic);
if (empty($realChildren)) {
return sprintf($this->prepare(self::PARENT_NO_CONTEXT, $level), $partialName);
}
return sprintf(
$this->prepare(self::PARENT, $level),
$partialName,
$this->walk($realChildren, $level + 1)
);
}
/**
* Helper method for filtering out non-block-arg tokens.
*
* @return bool True if $node is a block arg token
*/
private static function onlyBlockArgs(array $node)
{
return $node[Tokenizer::TYPE] === Tokenizer::T_BLOCK_ARG;
}
const VARIABLE = '
$value = $this->resolveValue($context->%s(%s%s), $context);%s
$buffer .= %s($value === null ? \'\' : %s);
';
/**
* Generate Mustache Template variable interpolation PHP source.
*
* @param string $id Variable name
* @param string[] $filters Array of filters
* @param bool $escape Escape the variable value for output?
* @param int $level
*
* @return string Generated variable interpolation PHP source
*/
private function variable($id, $filters, $escape, $level)
{
$method = $this->getFindMethod($id);
$id = ($method !== 'last') ? var_export($id, true) : '';
$findArg = $this->getFindMethodArgs($method);
$filters = $this->getFilters($filters, $level);
$value = $escape ? $this->getEscape() : '$value';
return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $findArg, $filters, $this->flushIndent(), $value);
}
const FILTER = '
$filter = $context->%s(%s%s);
if (!(%s)) {
throw new \\Mustache\\Exception\\UnknownFilterException(%s);
}
$value = call_user_func($filter, %s);%s
';
const FILTER_FIRST_VALUE = '$this->resolveValue($value, $context)';
const FILTER_VALUE = '$value';
/**
* Generate Mustache Template variable filtering PHP source.
*
* If the initial $value is a lambda it will be resolved before starting the filter chain.
*
* @param string[] $filters Array of filters
* @param int $level
* @param bool $first (default: false)
*
* @return string Generated filter PHP source
*/
private function getFilters(array $filters, $level, $first = true)
{
if (empty($filters)) {
return '';
}
$name = array_shift($filters);
$method = $this->getFindMethod($name);
$filter = ($method !== 'last') ? var_export($name, true) : '';
$findArg = $this->getFindMethodArgs($method);
$callable = $this->getCallable('$filter');
$msg = var_export($name, true);
$value = $first ? self::FILTER_FIRST_VALUE : self::FILTER_VALUE;
return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $findArg, $callable, $msg, $value, $this->getFilters($filters, $level, false));
}
const LINE = '$buffer .= "\n";';
const TEXT = '$buffer .= %s%s;';
/**
* Generate Mustache Template output Buffer call PHP source.
*
* @param string $text
* @param int $level
*
* @return string Generated output Buffer call PHP source
*/
private function text($text, $level)
{
$indentNextLine = (substr($text, -1) === "\n");
$code = sprintf($this->prepare(self::TEXT, $level), $this->flushIndent(), var_export($text, true));
$this->indentNextLine = $indentNextLine;
return $code;
}
/**
* Prepare PHP source code snippet for output.
*
* @param string $text
* @param int $bonus Additional indent level (default: 0)
* @param bool $prependNewline Prepend a newline to the snippet? (default: true)
* @param bool $appendNewline Append a newline to the snippet? (default: false)
*
* @return string PHP source code snippet
*/
private function prepare($text, $bonus = 0, $prependNewline = true, $appendNewline = false)
{
$text = ($prependNewline ? "\n" : '') . trim($text);
if ($prependNewline) {
$bonus++;
}
if ($appendNewline) {
$text .= "\n";
}
return preg_replace("/\n( {8})?/", "\n" . str_repeat(' ', $bonus * 4), $text);
}
const DEFAULT_ESCAPE = 'htmlspecialchars(%s, %s, %s)';
const CUSTOM_ESCAPE = 'call_user_func($this->mustache->getEscape(), %s)';
/**
* Get the current escaper.
*
* @param string $value (default: '$value')
*
* @return string Either a custom callback, or an inline call to `htmlspecialchars`
*/
private function getEscape($value = '$value')
{
if ($this->customEscape) {
return sprintf(self::CUSTOM_ESCAPE, $value);
}
return sprintf(self::DEFAULT_ESCAPE, $value, var_export($this->entityFlags, true), var_export($this->charset, true));
}
/**
* Select the appropriate Context `find` method for a given $id.
*
* The return value will be one of `find`, `findDot`, `findAnchoredDot` or `last`.
*
* @see \Mustache\Context::find
* @see \Mustache\Context::findDot
* @see \Mustache\Context::last
*
* @param string $id Variable name
*
* @return string `find` method name
*/
private function getFindMethod($id)
{
if ($id === '.') {
return 'last';
}
if (isset($this->pragmas[Engine::PRAGMA_ANCHORED_DOT]) && $this->pragmas[Engine::PRAGMA_ANCHORED_DOT]) {
if (substr($id, 0, 1) === '.') {
return 'findAnchoredDot';
}
}
if (strpos($id, '.') === false) {
return 'find';
}
return 'findDot';
}
/**
* Get the args needed for a given find method.
*
* In this case, it's "true" iff it's a "find dot" method and strict callables is enabled.
*
* @param string $method Find method name
*/
private function getFindMethodArgs($method)
{
if (($method === 'findDot' || $method === 'findAnchoredDot') && $this->strictCallables) {
return ', true';
}
return '';
}
const IS_CALLABLE = '!is_string(%s) && is_callable(%s)';
const STRICT_IS_CALLABLE = 'is_object(%s) && is_callable(%s)';
/**
* Helper function to compile strict vs lax "is callable" logic.
*
* @param string $variable (default: '$value')
*
* @return string "is callable" logic
*/
private function getCallable($variable = '$value')
{
$tpl = $this->strictCallables ? self::STRICT_IS_CALLABLE : self::IS_CALLABLE;
return sprintf($tpl, $variable, $variable);
}
const LINE_INDENT = '$indent . ';
/**
* Get the current $indent prefix to write to the buffer.
*
* @return string "$indent . " or ""
*/
private function flushIndent()
{
if (!$this->indentNextLine) {
return '';
}
$this->indentNextLine = false;
return self::LINE_INDENT;
}
}
-277
View File
@@ -1,277 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
use Mustache\Exception\InvalidArgumentException;
/**
* Mustache Template rendering Context.
*/
class Context
{
private $stack = [];
private $blockStack = [];
private $buggyPropertyShadowing = false;
/**
* Mustache rendering Context constructor.
*
* @param mixed $context Default rendering context (default: null)
* @param bool $buggyPropertyShadowing See Engine::getBuggyPropertyShadowing (default: false)
*/
public function __construct($context = null, $buggyPropertyShadowing = false)
{
if ($context !== null) {
$this->stack = [$context];
}
$this->buggyPropertyShadowing = $buggyPropertyShadowing;
}
/**
* Push a new Context frame onto the stack.
*
* @param mixed $value Object or array to use for context
*/
public function push($value)
{
array_push($this->stack, $value);
}
/**
* Push a new Context frame onto the block context stack.
*
* @param mixed $value Object or array to use for block context
*/
public function pushBlockContext($value)
{
array_push($this->blockStack, $value);
}
/**
* Pop the last Context frame from the stack.
*
* @return mixed Last Context frame (object or array)
*/
public function pop()
{
return array_pop($this->stack);
}
/**
* Pop the last block Context frame from the stack.
*
* @return mixed Last block Context frame (object or array)
*/
public function popBlockContext()
{
return array_pop($this->blockStack);
}
/**
* Get the last Context frame.
*
* @return mixed Last Context frame (object or array)
*/
public function last()
{
return end($this->stack);
}
/**
* Find a variable in the Context stack.
*
* Starting with the last Context frame (the context of the innermost section), and working back to the top-level
* rendering context, look for a variable with the given name:
*
* * If the Context frame is an associative array which contains the key $id, returns the value of that element.
* * If the Context frame is an object, this will check first for a public method, then a public property named
* $id. Failing both of these, it will try `__isset` and `__get` magic methods.
* * If a value named $id is not found in any Context frame, returns an empty string.
*
* @param string $id Variable name
*
* @return mixed Variable value, or '' if not found
*/
public function find($id)
{
return $this->findVariableInStack($id, $this->stack);
}
/**
* Find a 'dot notation' variable in the Context stack.
*
* Note that dot notation traversal bubbles through scope differently than the regular find method. After finding
* the initial chunk of the dotted name, each subsequent chunk is searched for only within the value of the previous
* result. For example, given the following context stack:
*
* $data = [
* 'name' => 'Fred',
* 'child' => [
* 'name' => 'Bob'
* ],
* ];
*
* ... and the Mustache following template:
*
* {{ child.name }}
*
* ... the `name` value is only searched for within the `child` value of the global Context, not within parent
* Context frames.
*
* @param string $id Dotted variable selector
* @param bool $strictCallables (default: false)
*
* @return mixed Variable value, or '' if not found
*/
public function findDot($id, $strictCallables = false)
{
$chunks = explode('.', $id);
$first = array_shift($chunks);
$value = $this->findVariableInStack($first, $this->stack);
// This wasn't really a dotted name, so we can just return the value.
if (empty($chunks)) {
return $value;
}
foreach ($chunks as $chunk) {
$isCallable = $strictCallables ? (is_object($value) && is_callable($value)) : (!is_string($value) && is_callable($value));
if ($isCallable) {
$value = $value();
} elseif ($value === '') {
return $value;
}
$value = $this->findVariableInStack($chunk, [$value]);
}
return $value;
}
/**
* Find an 'anchored dot notation' variable in the Context stack.
*
* This is the same as findDot(), except it looks in the top of the context
* stack for the first value, rather than searching the whole context stack
* and starting from there.
*
* @see Mustache\Context::findDot
*
* @throws InvalidArgumentException if given an invalid anchored dot $id
*
* @param string $id Dotted variable selector
*
* @return mixed Variable value, or '' if not found
*/
public function findAnchoredDot($id)
{
$chunks = explode('.', $id);
$first = array_shift($chunks);
if ($first !== '') {
throw new InvalidArgumentException(sprintf('Unexpected id for findAnchoredDot: %s', $id));
}
$value = $this->last();
foreach ($chunks as $chunk) {
if ($value === '') {
return $value;
}
$value = $this->findVariableInStack($chunk, [$value]);
}
return $value;
}
/**
* Find an argument in the block context stack.
*
* @param string $id
*
* @return mixed Variable value, or '' if not found
*/
public function findInBlock($id)
{
foreach ($this->blockStack as $context) {
if (array_key_exists($id, $context)) {
return $context[$id];
}
}
return '';
}
/**
* Helper function to find a variable in the Context stack.
*
* @see Mustache\Context::find
*
* @param string $id Variable name
* @param array $stack Context stack
*
* @return mixed Variable value, or '' if not found
*/
private function findVariableInStack($id, array $stack)
{
for ($i = count($stack) - 1; $i >= 0; $i--) {
$frame = &$stack[$i];
switch (gettype($frame)) {
case 'object':
if (!($frame instanceof \Closure)) {
// Note that is_callable() *will not work here*
// See https://github.com/bobthecow/mustache.php/wiki/Magic-Methods
if (method_exists($frame, $id)) {
return $frame->$id();
}
if (isset($frame->$id)) {
return $frame->$id;
}
// Preserve backwards compatibility with a property shadowing bug in
// Mustache.php <= 2.14.2
// See https://github.com/bobthecow/mustache.php/pull/410
if ($this->buggyPropertyShadowing) {
if ($frame instanceof \ArrayAccess && isset($frame[$id])) {
return $frame[$id];
}
} else {
if (property_exists($frame, $id)) {
$rp = new \ReflectionProperty($frame, $id);
if ($rp->isPublic()) {
return $frame->$id;
}
}
if ($frame instanceof \ArrayAccess && $frame->offsetExists($id)) {
return $frame[$id];
}
}
}
break;
case 'array':
if (array_key_exists($id, $frame)) {
return $frame[$id];
}
break;
}
}
return '';
}
}
-963
View File
@@ -1,963 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
use Mustache\Cache\FilesystemCache;
use Mustache\Cache\NoopCache;
use Mustache\Exception\InvalidArgumentException;
use Mustache\Exception\RuntimeException;
use Mustache\Exception\UnknownTemplateException;
use Mustache\Loader\ArrayLoader;
use Mustache\Loader\MutableLoader;
use Mustache\Loader\StringLoader;
use Psr\Log\LoggerInterface;
/**
* A Mustache implementation in PHP.
*
* {@link https://mustache.github.io}
*
* Mustache is a framework-agnostic logic-less templating language. It enforces separation of view
* logic from template files. In fact, it is not even possible to embed logic in the template.
*
* This is very, very rad.
*
* @author Justin Hileman {@link http://justinhileman.com}
*/
class Engine
{
const VERSION = '3.0.0';
const SPEC_VERSION = '1.4.3';
const PRAGMA_FILTERS = 'FILTERS';
const PRAGMA_ANCHORED_DOT = 'ANCHORED-DOT';
/**
* @deprecated PRAGMA_BLOCKS is now part of the Mustache spec, and is enabled by default
*/
const PRAGMA_BLOCKS = 'BLOCKS';
// Known pragmas
private static $knownPragmas = [
self::PRAGMA_FILTERS => true,
self::PRAGMA_ANCHORED_DOT => true,
self::PRAGMA_BLOCKS => true,
];
// Template cache
private $templates = [];
// Environment
private $templateClassPrefix = '__Mustache_';
private $cache;
private $lambdaCache;
private $cacheLambdaTemplates = false;
private $doubleRenderLambdas = false;
private $loader;
private $partialsLoader;
private $helpers;
private $escape;
private $entityFlags = ENT_COMPAT;
private $charset = 'UTF-8';
private $logger;
private $strictCallables = true;
private $pragmas = [];
private $delimiters;
private $buggyPropertyShadowing = false;
// Optional Mustache specs
private $dynamicNames = true;
private $inheritance = true;
private $lambdas = true;
// Services
private $tokenizer;
private $parser;
private $compiler;
/**
* Mustache class constructor.
*
* Passing an $options array allows overriding certain Mustache options during instantiation:
*
* $options = [
* // The class prefix for compiled templates. Defaults to '__Mustache_'.
* 'template_class_prefix' => '__MyTemplates_',
*
* // A Mustache cache instance or a cache directory string for compiled templates.
* // Mustache will not cache templates unless this is set.
* 'cache' => __DIR__.'/tmp/cache/mustache',
*
* // Override default permissions for cache files. Defaults to using the system-defined umask. It is
* // *strongly* recommended that you configure your umask properly rather than overriding permissions here.
* 'cache_file_mode' => 0666,
*
* // Optionally, enable caching for lambda section templates. This is generally not recommended, as lambda
* // sections are often too dynamic to benefit from caching.
* 'cache_lambda_templates' => true,
*
* // Customize the tag delimiters used by this engine instance. Note that overriding here changes the
* // delimiters used to parse all templates and partials loaded by this instance. To override just for a
* // single template, use an inline "change delimiters" tag at the start of the template file:
* //
* // {{=<% %>=}}
* //
* 'delimiters' => '<% %>',
*
* // A Mustache template loader instance. Uses a StringLoader if not specified.
* 'loader' => new \Mustache\Loader\FilesystemLoader(__DIR__.'/views'),
*
* // A Mustache loader instance for partials.
* 'partials_loader' => new \Mustache\Loader\FilesystemLoader(__DIR__.'/views/partials'),
*
* // An array of Mustache partials. Useful for quick-and-dirty string template loading, but not as
* // efficient or lazy as a Filesystem (or database) loader.
* 'partials' => ['foo' => file_get_contents(__DIR__.'/views/partials/foo.mustache')],
*
* // An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order
* // sections), or any other valid Mustache context value. They will be prepended to the context stack,
* // so they will be available in any template loaded by this Mustache instance.
* 'helpers' => ['i18n' => function ($text) {
* // do something translatey here...
* }],
*
* // An 'escape' callback, responsible for escaping double-mustache variables.
* 'escape' => function ($value) {
* return htmlspecialchars($buffer, ENT_COMPAT, 'UTF-8');
* },
*
* // Type argument for `htmlspecialchars`. Defaults to ENT_COMPAT. You may prefer ENT_QUOTES.
* 'entity_flags' => ENT_QUOTES,
*
* // Character set for `htmlspecialchars`. Defaults to 'UTF-8'. Use 'UTF-8'.
* 'charset' => 'ISO-8859-1',
*
* // A Mustache Logger instance. No logging will occur unless this is set. Using a PSR-3 compatible
* // logging library -- such as Monolog -- is highly recommended. A simple stream logger implementation is
* // available as well:
* 'logger' => new \Mustache\Logger\StreamLogger('php://stderr'),
*
*
* // OPTIONAL MUSTACHE FEATURES:
*
* // Enable dynamic names. By default, variables and sections like `{{*name}}` will be resolved dynamically.
* //
* // To disable dynamic name resolution, set this to false.
* 'dynamic_names' => true,
*
* // Enable template inheritance. By default, templates can extend other templates using the `{{< name}}` and
* // `{{$ block}}` tags.
* //
* // To disable inheritance, set this to false.
* 'inheritance' => true,
*
* // Enable lambda sections and values. By default, "lambdas" are enabled; if a variable resolves to a
* // callable value, that callable is called before interpolation. If a section name resolves to a callable
* // value, it is treated as a "higher order section", and the section content is passed to the callable
* // for processing prior to rendering.
* //
* // Note that the FILTERS pragma requires lambdas to function, so using FILTERS without lambdas enabled
* // will throw an invalid argument exception.
* //
* // To disable lambdas and higher order sections entirely, set this to false.
* 'lambdas' => true,
*
* // Enable pragmas across all templates, regardless of the presence of pragma tags in the individual
* // templates.
* 'pragmas' => [\Mustache\Engine::PRAGMA_FILTERS],
*
*
* // BACKWARDS COMPATIBILITY:
*
* // Only treat \Closure instances and invokable classes as callable. If true, values like
* // `['ClassName', 'methodName']` and `[$classInstance, 'methodName']`, which are traditionally
* // "callable" in PHP, are not called to resolve variables for interpolation or section contexts. This
* // helps protect against arbitrary code execution when user input is passed directly into the template.
* //
* // Defaults to true, but can be set to false to preserve Mustache.php v2.x behavior.
* //
* // THIS IS NOT RECOMMENDED.
* 'strict_callables' => true,
*
* // Enable buggy property shadowing. Per the Mustache spec, keys of a value higher in the context stack
* // shadow similarly named keys lower in the stack. For example, in the template
* // `{{# foo }}{{ bar }}{{/ foo }}` if the value for `foo` has a method, property, or key named `bar`, it
* // will prevent looking lower in the context stack for a another value named `bar`.
* //
* // Setting the value of an array key to null prevents lookups higher in the context stack. The behavior
* // should have been identical for object properties (and ArrayAccess) as well, but a bug in the context
* // lookup logic meant that a property which exists but is set to null would not prevent further context
* // lookup.
* //
* // This bug was fixed in Mustache.php v3.x, but the previous buggy behavior can be preserved by setting this
* // option to true.
* //
* // THIS IS NOT RECOMMENDED.
* 'buggy_property_shadowing' => false,
*
* // Double-render lambda return values. By default, the return value of higher order sections that are
* // rendered via the lambda helper will *not* be re-rendered.
* //
* // To preserve the behavior of Mustache.php v2.x, set this to true.
* //
* // THIS IS NOT RECOMMENDED.
* 'double_render_lambdas' => false,
* ];
*
* @throws InvalidArgumentException If `escape` option is not callable
* @throws InvalidArgumentException If `lambdas` is disabled but the `FILTERS` pragma is enabled
*/
public function __construct(array $options = [])
{
if (isset($options['template_class_prefix'])) {
if ((string) $options['template_class_prefix'] === '') {
throw new InvalidArgumentException('Mustache Constructor "template_class_prefix" must not be empty');
}
$this->templateClassPrefix = $options['template_class_prefix'];
}
if (isset($options['cache'])) {
$cache = $options['cache'];
if (is_string($cache)) {
$mode = isset($options['cache_file_mode']) ? $options['cache_file_mode'] : null;
$cache = new FilesystemCache($cache, $mode);
}
$this->setCache($cache);
}
if (isset($options['cache_lambda_templates'])) {
$this->cacheLambdaTemplates = (bool) $options['cache_lambda_templates'];
}
if (isset($options['loader'])) {
$this->setLoader($options['loader']);
}
if (isset($options['partials_loader'])) {
$this->setPartialsLoader($options['partials_loader']);
}
if (isset($options['partials'])) {
$this->setPartials($options['partials']);
}
if (isset($options['helpers'])) {
$this->setHelpers($options['helpers']);
}
if (isset($options['escape'])) {
if (!is_callable($options['escape'])) {
throw new InvalidArgumentException('Mustache Constructor "escape" option must be callable');
}
$this->escape = $options['escape'];
}
if (isset($options['entity_flags'])) {
$this->entityFlags = $options['entity_flags'];
}
if (isset($options['charset'])) {
$this->charset = $options['charset'];
}
if (isset($options['logger'])) {
$this->setLogger($options['logger']);
}
if (isset($options['delimiters'])) {
$this->delimiters = $options['delimiters'];
}
// Optional Mustache features
if (isset($options['dynamic_names'])) {
$this->dynamicNames = $options['dynamic_names'] !== false;
}
if (isset($options['inheritance'])) {
$this->inheritance = $options['inheritance'] !== false;
}
if (isset($options['lambdas'])) {
$this->lambdas = $options['lambdas'] !== false;
}
if (isset($options['pragmas'])) {
foreach ($options['pragmas'] as $pragma) {
if (!isset(self::$knownPragmas[$pragma])) {
throw new InvalidArgumentException(sprintf('Unknown pragma: "%s"', $pragma));
}
$this->pragmas[$pragma] = true;
}
}
if (!$this->lambdas && isset($this->pragmas[self::PRAGMA_FILTERS])) {
throw new InvalidArgumentException('The FILTERS pragma requires lambda support');
}
// Backwards compatibility
if (isset($options['strict_callables'])) {
$this->strictCallables = (bool) $options['strict_callables'];
}
if (isset($options['buggy_property_shadowing'])) {
$this->buggyPropertyShadowing = (bool) $options['buggy_property_shadowing'];
}
if (isset($options['double_render_lambdas'])) {
$this->doubleRenderLambdas = (bool) $options['double_render_lambdas'];
}
}
/**
* Shortcut 'render' invocation.
*
* Equivalent to calling `$mustache->loadTemplate($template)->render($context);`
*
* @see Mustache\Engine::loadTemplate
* @see Mustache\Template::render
*
* @param string $template
*
* @return string Rendered template
*/
public function render($template, $context = [])
{
return $this->loadTemplate($template)->render($context);
}
/**
* Get the current Mustache escape callback.
*
* @return callable|null
*/
public function getEscape()
{
return $this->escape;
}
/**
* Get the current Mustache entity type to escape.
*
* @return int
*/
public function getEntityFlags()
{
return $this->entityFlags;
}
/**
* Get the current Mustache character set.
*
* @return string
*/
public function getCharset()
{
return $this->charset;
}
/**
* Check whether to double-render higher-order sections.
*
* By default, the return value of higher order sections that are rendered
* via the lambda helper will *not* be re-rendered. To preserve the
* behavior of Mustache.php v2.x, set this to true.
*
* THIS IS NOT RECOMMENDED.
*/
public function getDoubleRenderLambdas()
{
return $this->doubleRenderLambdas;
}
/**
* Check whether to use buggy property shadowing.
*
* THIS IS NOT RECOMMENDED.
*
* See https://github.com/bobthecow/mustache.php/pull/410
*/
public function getBuggyPropertyShadowing()
{
return $this->buggyPropertyShadowing;
}
/**
* Get currently enabled optional features.
*
* @return array
*/
public function getOptions()
{
return [
'dynamic_names' => $this->dynamicNames,
'inheritance' => $this->inheritance,
'lambdas' => $this->lambdas,
];
}
/**
* Get the current globally enabled pragmas.
*
* @return array
*/
public function getPragmas()
{
return array_keys($this->pragmas);
}
/**
* Set the Mustache template Loader instance.
*/
public function setLoader(Loader $loader)
{
$this->loader = $loader;
}
/**
* Get the current Mustache template Loader instance.
*
* If no Loader instance has been explicitly specified, this method will instantiate and return
* a StringLoader instance.
*
* @return Loader
*/
public function getLoader()
{
if (!isset($this->loader)) {
$this->loader = new StringLoader();
}
return $this->loader;
}
/**
* Set the Mustache partials Loader instance.
*/
public function setPartialsLoader(Loader $partialsLoader)
{
$this->partialsLoader = $partialsLoader;
}
/**
* Get the current Mustache partials Loader instance.
*
* If no Loader instance has been explicitly specified, this method will instantiate and return
* an ArrayLoader instance.
*
* @return Loader
*/
public function getPartialsLoader()
{
if (!isset($this->partialsLoader)) {
$this->partialsLoader = new ArrayLoader();
}
return $this->partialsLoader;
}
/**
* Set partials for the current partials Loader instance.
*
* @throws RuntimeException If the current Loader instance is immutable
*/
public function setPartials(array $partials = [])
{
if (!isset($this->partialsLoader)) {
$this->partialsLoader = new ArrayLoader();
}
if (!$this->partialsLoader instanceof MutableLoader) {
throw new RuntimeException('Unable to set partials on an immutable Mustache Loader instance');
}
$this->partialsLoader->setTemplates($partials);
}
/**
* Set an array of Mustache helpers.
*
* An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order sections), or
* any other valid Mustache context value. They will be prepended to the context stack, so they will be available in
* any template loaded by this Mustache instance.
*
* @throws InvalidArgumentException if $helpers is not an array or \Traversable
*
* @param array|\Traversable $helpers
*/
public function setHelpers($helpers)
{
if (!is_array($helpers) && !$helpers instanceof \Traversable) {
throw new InvalidArgumentException('setHelpers expects an array of helpers');
}
$this->getHelpers()->clear();
foreach ($helpers as $name => $helper) {
$this->addHelper($name, $helper);
}
}
/**
* Get the current set of Mustache helpers.
*
* @see Mustache\Engine::setHelpers
*
* @return HelperCollection
*/
public function getHelpers()
{
if (!isset($this->helpers)) {
$this->helpers = new HelperCollection();
}
return $this->helpers;
}
/**
* Add a new Mustache helper.
*
* @see Mustache\Engine::setHelpers
*
* @param string $name
* @param mixed $helper
*/
public function addHelper($name, $helper)
{
$this->getHelpers()->add($name, $helper);
}
/**
* Get a Mustache helper by name.
*
* @see Mustache\Engine::setHelpers
*
* @param string $name
*
* @return mixed Helper
*/
public function getHelper($name)
{
return $this->getHelpers()->get($name);
}
/**
* Check whether this Mustache instance has a helper.
*
* @see Mustache\Engine::setHelpers
*
* @param string $name
*
* @return bool True if the helper is present
*/
public function hasHelper($name)
{
return $this->getHelpers()->has($name);
}
/**
* Remove a helper by name.
*
* @see Mustache\Engine::setHelpers
*
* @param string $name
*/
public function removeHelper($name)
{
$this->getHelpers()->remove($name);
}
/**
* Set the Mustache Logger instance.
*
* @throws InvalidArgumentException If logger is not an instance of Mustache\Logger or Psr\Log\LoggerInterface
*
* @param Logger|LoggerInterface $logger
*/
public function setLogger($logger = null)
{
// n.b. this uses `is_a` to prevent a dependency on Psr\Log
if ($logger !== null && !$logger instanceof Logger && !is_a($logger, 'Psr\\Log\\LoggerInterface')) {
throw new InvalidArgumentException('Expected an instance of Mustache\\Logger or Psr\\Log\\LoggerInterface.');
}
if ($this->getCache()->getLogger() === null) {
$this->getCache()->setLogger($logger);
}
$this->logger = $logger;
}
/**
* Get the current Mustache Logger instance.
*
* @return Logger|LoggerInterface
*/
public function getLogger()
{
return $this->logger;
}
/**
* Set the Mustache Tokenizer instance.
*/
public function setTokenizer(Tokenizer $tokenizer)
{
$this->tokenizer = $tokenizer;
}
/**
* Get the current Mustache Tokenizer instance.
*
* If no Tokenizer instance has been explicitly specified, this method will instantiate and return a new one.
*
* @return Tokenizer
*/
public function getTokenizer()
{
if (!isset($this->tokenizer)) {
$this->tokenizer = new Tokenizer();
}
return $this->tokenizer;
}
/**
* Set the Mustache Parser instance.
*/
public function setParser(Parser $parser)
{
$this->parser = $parser;
}
/**
* Get the current Mustache Parser instance.
*
* If no Parser instance has been explicitly specified, this method will instantiate and return a new one.
*
* @return Parser
*/
public function getParser()
{
if (!isset($this->parser)) {
$this->parser = new Parser();
}
return $this->parser;
}
/**
* Set the Mustache Compiler instance.
*/
public function setCompiler(Compiler $compiler)
{
$this->compiler = $compiler;
}
/**
* Get the current Mustache Compiler instance.
*
* If no Compiler instance has been explicitly specified, this method will instantiate and return a new one.
*
* @return Compiler
*/
public function getCompiler()
{
if (!isset($this->compiler)) {
$this->compiler = new Compiler();
}
return $this->compiler;
}
/**
* Set the Mustache Cache instance.
*/
public function setCache(Cache $cache)
{
if (isset($this->logger) && $cache->getLogger() === null) {
$cache->setLogger($this->getLogger());
}
$this->cache = $cache;
}
/**
* Get the current Mustache Cache instance.
*
* If no Cache instance has been explicitly specified, this method will instantiate and return a new one.
*
* @return Cache
*/
public function getCache()
{
if (!isset($this->cache)) {
$this->setCache(new NoopCache());
}
return $this->cache;
}
/**
* Get the current Lambda Cache instance.
*
* If 'cache_lambda_templates' is enabled, this is the default cache instance. Otherwise, it is a NoopCache.
*
* @see Mustache\Engine::getCache
*
* @return Cache
*/
protected function getLambdaCache()
{
if ($this->cacheLambdaTemplates) {
return $this->getCache();
}
if (!isset($this->lambdaCache)) {
$this->lambdaCache = new NoopCache();
}
return $this->lambdaCache;
}
/**
* Helper method to generate a Mustache template class.
*
* This method must be updated any time options are added which make it so
* the same template could be parsed and compiled multiple different ways.
*
* @param string|Source $source
*
* @return string Mustache Template class name
*/
public function getTemplateClassName($source)
{
// For the most part, adding a new option here should do the trick.
//
// Pick a value here which is unique for each possible way the template
// could be compiled... but not necessarily unique per option value. See
// escape below, which only needs to differentiate between 'custom' and
// 'default' escapes.
//
// Keep this list in alphabetical order :)
$chunks = [
'charset' => $this->charset,
'delimiters' => $this->delimiters ?: '{{ }}',
'entityFlags' => $this->entityFlags,
'escape' => isset($this->escape) ? 'custom' : 'default',
'key' => ($source instanceof Source) ? $source->getKey() : 'source',
'options' => $this->getOptions(),
'pragmas' => $this->getPragmas(),
'strictCallables' => $this->strictCallables,
'version' => self::VERSION,
];
$key = json_encode($chunks);
// Template Source instances have already provided their own source key. For strings, just include the whole
// source string in the md5 hash.
if (!$source instanceof Source) {
$key .= "\n" . $source;
}
return $this->templateClassPrefix . md5($key);
}
/**
* Load a Mustache Template by name.
*
* @param string $name
*
* @return Template
*/
public function loadTemplate($name)
{
return $this->loadSource($this->getLoader()->load($name));
}
/**
* Load a Mustache partial Template by name.
*
* This is a helper method used internally by Template instances for loading partial templates. You can most likely
* ignore it completely.
*
* @param string $name
*
* @return Template
*/
public function loadPartial($name)
{
try {
if (isset($this->partialsLoader)) {
$loader = $this->partialsLoader;
} elseif (isset($this->loader) && !$this->loader instanceof StringLoader) {
$loader = $this->loader;
} else {
throw new UnknownTemplateException($name);
}
return $this->loadSource($loader->load($name));
} catch (UnknownTemplateException $e) {
// If the named partial cannot be found, log then return null.
$this->log(
Logger::WARNING,
'Partial not found: "{name}"',
['name' => $e->getTemplateName()]
);
}
}
/**
* Load a Mustache lambda Template by source.
*
* This is a helper method used by Template instances to generate subtemplates for Lambda sections. You can most
* likely ignore it completely.
*
* @param string $source
* @param string $delims (default: null)
*
* @return Template
*/
public function loadLambda($source, $delims = null)
{
if ($delims !== null) {
$source = $delims . "\n" . $source;
}
return $this->loadSource($source, $this->getLambdaCache());
}
/**
* Instantiate and return a Mustache Template instance by source.
*
* Optionally provide a Mustache\Cache instance. This is used internally by Mustache\Engine::loadLambda to respect
* the 'cache_lambda_templates' configuration option.
*
* @see Mustache\Engine::loadTemplate
* @see Mustache\Engine::loadPartial
* @see Mustache\Engine::loadLambda
*
* @param string|Source $source
* @param Cache $cache (default: null)
*
* @return Template
*/
private function loadSource($source, $cache = null)
{
$className = $this->getTemplateClassName($source);
if (!isset($this->templates[$className])) {
if ($cache === null || !$cache instanceof Cache) {
$cache = $this->getCache();
}
if (!class_exists($className, false)) {
if (!$cache->load($className)) {
$compiled = $this->compile($source);
$cache->cache($className, $compiled);
}
}
$this->log(
Logger::DEBUG,
'Instantiating template: "{className}"',
['className' => $className]
);
$this->templates[$className] = new $className($this);
}
return $this->templates[$className];
}
/**
* Helper method to tokenize a Mustache template.
*
* @see Mustache\Tokenizer::scan
*
* @param string $source
*
* @return array Tokens
*/
private function tokenize($source)
{
return $this->getTokenizer()->scan($source, $this->delimiters);
}
/**
* Helper method to parse a Mustache template.
*
* @see Mustache\Parser::parse
*
* @param string $source
*
* @return array Token tree
*/
private function parse($source)
{
$parser = $this->getParser();
$parser->setOptions($this->getOptions());
$parser->setPragmas($this->getPragmas());
return $parser->parse($this->tokenize($source));
}
/**
* Helper method to compile a Mustache template.
*
* @see Mustache\Compiler::compile
*
* @param string|Source $source
*
* @return string generated Mustache template class code
*/
private function compile($source)
{
$name = $this->getTemplateClassName($source);
$this->log(
Logger::INFO,
'Compiling template to "{className}" class',
['className' => $name]
);
if ($source instanceof Source) {
$source = $source->getSource();
}
$tree = $this->parse($source);
$compiler = $this->getCompiler();
$compiler->setOptions($this->getOptions());
$compiler->setPragmas($this->getPragmas());
return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags);
}
/**
* Add a log record if logging is enabled.
*
* @param int $level The logging level
* @param string $message The log message
* @param array $context The log context
*/
private function log($level, $message, array $context = [])
{
if (isset($this->logger)) {
$this->logger->log($level, $message, $context);
}
}
}
-17
View File
@@ -1,17 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
interface Exception
{
// This space intentionally left blank.
}
@@ -1,22 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Exception;
use Mustache\Exception;
/**
* Invalid argument exception.
*/
class InvalidArgumentException extends \InvalidArgumentException implements Exception
{
// This space intentionally left blank.
}
@@ -1,22 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Exception;
use Mustache\Exception;
/**
* Logic exception.
*/
class LogicException extends \LogicException implements Exception
{
// This space intentionally left blank.
}
@@ -1,22 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Exception;
use Mustache\Exception;
/**
* Runtime exception.
*/
class RuntimeException extends \RuntimeException implements Exception
{
// This space intentionally left blank.
}
@@ -1,40 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Exception;
use Mustache\Exception;
/**
* Mustache syntax exception.
*/
class SyntaxException extends LogicException implements Exception
{
protected $token;
/**
* @param string $msg
* @param Exception $previous
*/
public function __construct($msg, array $token, $previous = null)
{
$this->token = $token;
parent::__construct($msg, 0, $previous);
}
/**
* @return array
*/
public function getToken()
{
return $this->token;
}
}
@@ -1,38 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Exception;
use Mustache\Exception;
/**
* Unknown filter exception.
*/
class UnknownFilterException extends \UnexpectedValueException implements Exception
{
protected $filterName;
/**
* @param string $filterName
* @param Exception $previous
*/
public function __construct($filterName, $previous = null)
{
$this->filterName = $filterName;
$message = sprintf('Unknown filter: %s', $filterName);
parent::__construct($message, 0, $previous);
}
public function getFilterName()
{
return $this->filterName;
}
}
@@ -1,38 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Exception;
use Mustache\Exception;
/**
* Unknown helper exception.
*/
class UnknownHelperException extends InvalidArgumentException implements Exception
{
protected $helperName;
/**
* @param string $helperName
* @param Exception $previous
*/
public function __construct($helperName, $previous = null)
{
$this->helperName = $helperName;
$message = sprintf('Unknown helper: %s', $helperName);
parent::__construct($message, 0, $previous);
}
public function getHelperName()
{
return $this->helperName;
}
}
@@ -1,38 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Exception;
use Mustache\Exception;
/**
* Unknown template exception.
*/
class UnknownTemplateException extends InvalidArgumentException implements Exception
{
protected $templateName;
/**
* @param string $templateName
* @param Exception $previous
*/
public function __construct($templateName, $previous = null)
{
$this->templateName = $templateName;
$message = sprintf('Unknown template: %s', $templateName);
parent::__construct($message, 0, $previous);
}
public function getTemplateName()
{
return $this->templateName;
}
}
-177
View File
@@ -1,177 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
use Mustache\Exception\InvalidArgumentException;
use Mustache\Exception\UnknownHelperException;
/**
* A collection of helpers for a Mustache instance.
*/
class HelperCollection
{
private $helpers = [];
/**
* Helper Collection constructor.
*
* Optionally accepts an array (or \Traversable) of `$name => $helper` pairs.
*
* @throws InvalidArgumentException if the $helpers argument isn't an array or \Traversable
*
* @param array|\Traversable $helpers (default: null)
*/
public function __construct($helpers = null)
{
if ($helpers === null) {
return;
}
if (!is_array($helpers) && !$helpers instanceof \Traversable) {
throw new InvalidArgumentException('HelperCollection constructor expects an array of helpers');
}
foreach ($helpers as $name => $helper) {
$this->add($name, $helper);
}
}
/**
* Magic mutator.
*
* @see Mustache\HelperCollection::add
*
* @param string $name
* @param mixed $helper
*/
public function __set($name, $helper)
{
$this->add($name, $helper);
}
/**
* Add a helper to this collection.
*
* @param string $name
* @param mixed $helper
*/
public function add($name, $helper)
{
$this->helpers[$name] = $helper;
}
/**
* Magic accessor.
*
* @see Mustache\HelperCollection::get
*
* @param string $name
*
* @return mixed Helper
*/
public function __get($name)
{
return $this->get($name);
}
/**
* Get a helper by name.
*
* @throws UnknownHelperException If helper does not exist
*
* @param string $name
*
* @return mixed Helper
*/
public function get($name)
{
if (!$this->has($name)) {
throw new UnknownHelperException($name);
}
return $this->helpers[$name];
}
/**
* Magic isset().
*
* @see Mustache\HelperCollection::has
*
* @param string $name
*
* @return bool True if helper is present
*/
public function __isset($name)
{
return $this->has($name);
}
/**
* Check whether a given helper is present in the collection.
*
* @param string $name
*
* @return bool True if helper is present
*/
public function has($name)
{
return array_key_exists($name, $this->helpers);
}
/**
* Magic unset().
*
* @see Mustache\HelperCollection::remove
*
* @param string $name
*/
public function __unset($name)
{
$this->remove($name);
}
/**
* Check whether a given helper is present in the collection.
*
* @throws UnknownHelperException if the requested helper is not present
*
* @param string $name
*/
public function remove($name)
{
if (!$this->has($name)) {
throw new UnknownHelperException($name);
}
unset($this->helpers[$name]);
}
/**
* Clear the helper collection.
*
* Removes all helpers from this collection
*/
public function clear()
{
$this->helpers = [];
}
/**
* Check whether the helper collection is empty.
*
* @return bool True if the collection is empty
*/
public function isEmpty()
{
return empty($this->helpers);
}
}
-96
View File
@@ -1,96 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
/**
* Mustache Lambda Helper.
*
* Passed as the second argument to section lambdas (higher order sections),
* giving them access to a `render` method for rendering a string with the
* current context.
*/
class LambdaHelper
{
private $mustache;
private $context;
private $delims;
/**
* Mustache Lambda Helper constructor.
*
* @param Engine $mustache Mustache engine instance
* @param Context $context Rendering context
* @param string $delims Optional custom delimiters, in the format `{{= <% %> =}}`. (default: null)
*/
public function __construct(Engine $mustache, Context $context, $delims = null)
{
$this->mustache = $mustache;
$this->context = $context;
$this->delims = $delims;
}
/**
* Render a string as a Mustache template with the current rendering context.
*
* @param string $string
*
* @return string Rendered template
*/
public function render($string)
{
$value = $this->mustache
->loadLambda((string) $string, $this->delims)
->renderInternal($this->context);
return $this->mustache->getDoubleRenderLambdas() ? $value : $this->preventRender($value);
}
/**
* Prevent rendering of a string as a Mustache template.
*
* This is useful for returning a raw string from a lambda without processing it as a Mustache template.
*
* @see RenderedString
*
* @param string $value The raw string value to return
*
* @return RenderedString A RenderedString instance containing the raw value
*/
public function preventRender($value)
{
return new RenderedString($value);
}
/**
* Render a string as a Mustache template with the current rendering context.
*
* @param string $string
*
* @return string Rendered template
*/
public function __invoke($string)
{
return $this->render($string);
}
/**
* Get a Lambda Helper with custom delimiters.
*
* @param string $delims Custom delimiters, in the format `{{= <% %> =}}`
*
* @return LambdaHelper
*/
public function withDelimiters($delims)
{
return new self($this->mustache, $this->context, $delims);
}
}
-28
View File
@@ -1,28 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
use Mustache\Exception\UnknownTemplateException;
interface Loader
{
/**
* Load a Template by name.
*
* @throws UnknownTemplateException If a template file is not found
*
* @param string $name
*
* @return string|Source Mustache Template source
*/
public function load($name);
}
-82
View File
@@ -1,82 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Loader;
use Mustache\Exception\UnknownTemplateException;
use Mustache\Loader;
/**
* Mustache Template array Loader implementation.
*
* An ArrayLoader instance loads Mustache Template source by name from an initial array:
*
* $loader = new ArrayLoader(
* 'foo' => '{{ bar }}',
* 'baz' => 'Hey {{ qux }}!'
* );
*
* $tpl = $loader->load('foo'); // '{{ bar }}'
*
* The ArrayLoader is used internally as a partials loader by Mustache\Engine instance when an array of partials
* is set. It can also be used as a quick-and-dirty Template loader.
*/
class ArrayLoader implements Loader, MutableLoader
{
private $templates;
/**
* ArrayLoader constructor.
*
* @param array $templates Associative array of Template source (default: [])
*/
public function __construct(array $templates = [])
{
$this->templates = $templates;
}
/**
* Load a Template.
*
* @throws UnknownTemplateException If a template file is not found
*
* @param string $name
*
* @return string Mustache Template source
*/
public function load($name)
{
if (!isset($this->templates[$name])) {
throw new UnknownTemplateException($name);
}
return $this->templates[$name];
}
/**
* Set an associative array of Template sources for this loader.
*/
public function setTemplates(array $templates)
{
$this->templates = $templates;
}
/**
* Set a Template source by name.
*
* @param string $name
* @param string $template Mustache Template source
*/
public function setTemplate($name, $template)
{
$this->templates[$name] = $template;
}
}
-72
View File
@@ -1,72 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Loader;
use Mustache\Exception\UnknownTemplateException;
use Mustache\Loader;
/**
* A Mustache Template cascading loader implementation, which delegates to other
* Loader instances.
*/
class CascadingLoader implements Loader
{
private $loaders;
/**
* Construct a CascadingLoader with an array of loaders.
*
* $loader = new CascadingLoader([
* new InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__),
* new FilesystemLoader(__DIR__.'/templates')
* ]);
*
* @param Loader[] $loaders
*/
public function __construct(array $loaders = [])
{
$this->loaders = [];
foreach ($loaders as $loader) {
$this->addLoader($loader);
}
}
/**
* Add a Loader instance.
*/
public function addLoader(Loader $loader)
{
$this->loaders[] = $loader;
}
/**
* Load a Template by name.
*
* @throws UnknownTemplateException If a template file is not found
*
* @param string $name
*
* @return string Mustache Template source
*/
public function load($name)
{
foreach ($this->loaders as $loader) {
try {
return $loader->load($name);
} catch (UnknownTemplateException $e) {
// do nothing, check the next loader.
}
}
throw new UnknownTemplateException($name);
}
}
-141
View File
@@ -1,141 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Loader;
use Mustache\Exception\RuntimeException;
use Mustache\Exception\UnknownTemplateException;
use Mustache\Loader;
/**
* Mustache Template filesystem Loader implementation.
*
* A FilesystemLoader instance loads Mustache Template source from the filesystem by name:
*
* $loader = new FilesystemLoader(__DIR__.'/views');
* $tpl = $loader->load('foo'); // equivalent to `file_get_contents(__DIR__.'/views/foo.mustache');
*
* This is probably the most useful Mustache Loader implementation. It can be used for partials and normal Templates:
*
* $m = new \Mustache\Engine([
* 'loader' => new FilesystemLoader(__DIR__.'/views'),
* 'partials_loader' => new FilesystemLoader(__DIR__.'/views/partials'),
* ]);
*/
class FilesystemLoader implements Loader
{
private $baseDir;
private $extension = '.mustache';
private $templates = [];
/**
* Mustache filesystem Loader constructor.
*
* Passing an $options array allows overriding certain Loader options during instantiation:
*
* $options = [
* // The filename extension used for Mustache templates. Defaults to '.mustache'
* 'extension' => '.ms',
* ];
*
* @throws RuntimeException if $baseDir does not exist
*
* @param string $baseDir Base directory containing Mustache template files
* @param array $options Loader options (default: [])
*/
public function __construct($baseDir, array $options = [])
{
$this->baseDir = $baseDir;
if (strpos($this->baseDir, '://') === false) {
$this->baseDir = realpath($this->baseDir);
}
if ($this->shouldCheckPath() && !is_dir($this->baseDir)) {
throw new RuntimeException(sprintf('FilesystemLoader baseDir must be a directory: %s', $baseDir));
}
if (array_key_exists('extension', $options)) {
if (empty($options['extension'])) {
$this->extension = '';
} else {
$this->extension = '.' . ltrim($options['extension'], '.');
}
}
}
/**
* Load a Template by name.
*
* $loader = new FilesystemLoader(__DIR__.'/views');
* $loader->load('admin/dashboard'); // loads "./views/admin/dashboard.mustache";
*
* @param string $name
*
* @return string Mustache Template source
*/
public function load($name)
{
if (!isset($this->templates[$name])) {
$this->templates[$name] = $this->loadFile($name);
}
return $this->templates[$name];
}
/**
* Helper function for loading a Mustache file by name.
*
* @throws UnknownTemplateException If a template file is not found
*
* @param string $name
*
* @return string Mustache Template source
*/
protected function loadFile($name)
{
$fileName = $this->getFileName($name);
if ($this->shouldCheckPath() && !file_exists($fileName)) {
throw new UnknownTemplateException($name);
}
return file_get_contents($fileName);
}
/**
* Helper function for getting a Mustache template file name.
*
* @param string $name
*
* @return string Template file name
*/
protected function getFileName($name)
{
$fileName = $this->baseDir . '/' . $name;
if (substr($fileName, 0 - strlen($this->extension)) !== $this->extension) {
$fileName .= $this->extension;
}
return $fileName;
}
/**
* Only check if baseDir is a directory and requested templates are files if
* baseDir is using the filesystem stream wrapper.
*
* @return bool Whether to check `is_dir` and `file_exists`
*/
protected function shouldCheckPath()
{
return strpos($this->baseDir, '://') === false || strpos($this->baseDir, 'file://') === 0;
}
}
-129
View File
@@ -1,129 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Loader;
use Mustache\Exception\InvalidArgumentException;
use Mustache\Exception\UnknownTemplateException;
use Mustache\Loader;
/**
* A Mustache Template loader for inline templates.
*
* With the InlineLoader, templates can be defined at the end of any PHP source
* file:
*
* $loader = new InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__);
* $hello = $loader->load('hello');
* $goodbye = $loader->load('goodbye');
*
* __halt_compiler();
*
* @@ hello
* Hello, {{ planet }}!
*
* @@ goodbye
* Goodbye, cruel {{ planet }}
*
* Templates are deliniated by lines containing only `@@ name`.
*
* The InlineLoader is well-suited to micro-frameworks such as Silex:
*
* $app->register(new MustacheServiceProvider, [
* 'mustache.loader' => new InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__)
* ]);
*
* $app->get('/{name}', function ($name) use ($app) {
* return $app['mustache']->render('hello', compact('name'));
* })
* ->value('name', 'world');
*
* // ...
*
* __halt_compiler();
*
* @@ hello
* Hello, {{ name }}!
*/
class InlineLoader implements Loader
{
protected $fileName;
protected $offset;
protected $templates;
/**
* The InlineLoader requires a filename and offset to process templates.
*
* The magic constants `__FILE__` and `__COMPILER_HALT_OFFSET__` are usually
* perfectly suited to the job:
*
* $loader = new InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__);
*
* Note that this only works if the loader is instantiated inside the same
* file as the inline templates. If the templates are located in another
* file, it would be necessary to manually specify the filename and offset.
*
* @param string $fileName The file to parse for inline templates
* @param int $offset A string offset for the start of the templates.
* This usually coincides with the `__halt_compiler`
* call, and the `__COMPILER_HALT_OFFSET__`
*/
public function __construct($fileName, $offset)
{
if (!is_file($fileName)) {
throw new InvalidArgumentException('InlineLoader expects a valid filename.');
}
if (!is_int($offset) || $offset < 0) {
throw new InvalidArgumentException('InlineLoader expects a valid file offset.');
}
$this->fileName = $fileName;
$this->offset = $offset;
}
/**
* Load a Template by name.
*
* @throws UnknownTemplateException If a template file is not found
*
* @param string $name
*
* @return string Mustache Template source
*/
public function load($name)
{
$this->loadTemplates();
if (!array_key_exists($name, $this->templates)) {
throw new UnknownTemplateException($name);
}
return $this->templates[$name];
}
/**
* Parse and load templates from the end of a source file.
*/
protected function loadTemplates()
{
if ($this->templates === null) {
$this->templates = [];
$data = file_get_contents($this->fileName, false, null, $this->offset);
foreach (preg_split("/^@@(?= [\w\d\.]+$)/m", $data, -1) as $chunk) {
if (trim($chunk) !== '') {
list($name, $content) = explode("\n", $chunk, 2);
$this->templates[trim($name)] = trim($content);
}
}
}
}
}
-28
View File
@@ -1,28 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Loader;
interface MutableLoader
{
/**
* Set an associative array of Template sources for this loader.
*/
public function setTemplates(array $templates);
/**
* Set a Template source by name.
*
* @param string $name
* @param string $template Mustache Template source
*/
public function setTemplate($name, $template);
}
@@ -1,93 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Loader;
use Mustache\Exception\RuntimeException;
use Mustache\Exception\UnknownTemplateException;
use Mustache\Source;
use Mustache\Source\FilesystemSource;
/**
* Mustache Template production filesystem Loader implementation.
*
* A production-ready FilesystemLoader, which doesn't require reading a file if it already exists in the template cache.
*
* {@inheritdoc}
*/
class ProductionFilesystemLoader extends FilesystemLoader
{
private $statProps;
/**
* Mustache production filesystem Loader constructor.
*
* Passing an $options array allows overriding certain Loader options during instantiation:
*
* $options = [
* // The filename extension used for Mustache templates. Defaults to '.mustache'
* 'extension' => '.ms',
* 'stat_props' => ['size', 'mtime'],
* ];
*
* Specifying 'stat_props' overrides the stat properties used to invalidate the template cache. By default, this
* uses 'mtime' and 'size', but this can be set to any of the properties supported by stat():
*
* http://php.net/manual/en/function.stat.php
*
* You can also disable filesystem stat entirely:
*
* $options = ['stat_props' => null];
*
* But with great power comes great responsibility. Namely, if you disable stat-based cache invalidation,
* YOU MUST CLEAR THE TEMPLATE CACHE YOURSELF when your templates change. Make it part of your build or deploy
* process so you don't forget!
*
* @throws RuntimeException if $baseDir does not exist
*
* @param string $baseDir base directory containing Mustache template files
* @param array $options Loader options (default: [])
*/
public function __construct($baseDir, array $options = [])
{
parent::__construct($baseDir, $options);
if (array_key_exists('stat_props', $options)) {
if (empty($options['stat_props'])) {
$this->statProps = [];
} else {
$this->statProps = $options['stat_props'];
}
} else {
$this->statProps = ['size', 'mtime'];
}
}
/**
* Helper function for loading a Mustache file by name.
*
* @throws UnknownTemplateException if a template file is not found
*
* @param string $name
*
* @return Source Mustache Template source
*/
protected function loadFile($name)
{
$fileName = $this->getFileName($name);
if (!file_exists($fileName)) {
throw new UnknownTemplateException($name);
}
return new FilesystemSource($fileName, $this->statProps);
}
}
-43
View File
@@ -1,43 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Loader;
use Mustache\Loader;
/**
* Mustache Template string Loader implementation.
*
* A StringLoader instance is essentially a noop. It simply passes the 'name' argument straight through:
*
* $loader = new StringLoader;
* $tpl = $loader->load('{{ foo }}'); // '{{ foo }}'
*
* This is the default Template Loader instance used by Mustache:
*
* $m = new \Mustache\Engine;
* $tpl = $m->loadTemplate('{{ foo }}');
* echo $tpl->render(['foo' => 'bar']); // "bar"
*/
class StringLoader implements Loader
{
/**
* Load a Template by source.
*
* @param string $name Mustache Template source
*
* @return string Mustache Template source
*/
public function load($name)
{
return $name;
}
}
-102
View File
@@ -1,102 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
interface Logger
{
/**
* Psr\Log compatible log levels.
*/
const EMERGENCY = 'emergency';
const ALERT = 'alert';
const CRITICAL = 'critical';
const ERROR = 'error';
const WARNING = 'warning';
const NOTICE = 'notice';
const INFO = 'info';
const DEBUG = 'debug';
/**
* System is unusable.
*
* @param string $message
*/
public function emergency($message, array $context = []);
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string $message
*/
public function alert($message, array $context = []);
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
*/
public function critical($message, array $context = []);
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
*/
public function error($message, array $context = []);
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string $message
*/
public function warning($message, array $context = []);
/**
* Normal but significant events.
*
* @param string $message
*/
public function notice($message, array $context = []);
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message
*/
public function info($message, array $context = []);
/**
* Detailed debug information.
*
* @param string $message
*/
public function debug($message, array $context = []);
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
*/
public function log($level, $message, array $context = []);
}
-117
View File
@@ -1,117 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Logger;
use Mustache\Logger;
/**
* This is a simple Logger implementation that other Loggers can inherit from.
*
* This is identical to the Psr\Log\AbstractLogger.
*
* It simply delegates all log-level-specific methods to the `log` method to
* reduce boilerplate code that a simple Logger that does the same thing with
* messages regardless of the error level has to implement.
*/
abstract class AbstractLogger implements Logger
{
/**
* System is unusable.
*
* @param string $message
*/
public function emergency($message, array $context = [])
{
$this->log(Logger::EMERGENCY, $message, $context);
}
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string $message
*/
public function alert($message, array $context = [])
{
$this->log(Logger::ALERT, $message, $context);
}
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
*/
public function critical($message, array $context = [])
{
$this->log(Logger::CRITICAL, $message, $context);
}
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
*/
public function error($message, array $context = [])
{
$this->log(Logger::ERROR, $message, $context);
}
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string $message
*/
public function warning($message, array $context = [])
{
$this->log(Logger::WARNING, $message, $context);
}
/**
* Normal but significant events.
*
* @param string $message
*/
public function notice($message, array $context = [])
{
$this->log(Logger::NOTICE, $message, $context);
}
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message
*/
public function info($message, array $context = [])
{
$this->log(Logger::INFO, $message, $context);
}
/**
* Detailed debug information.
*
* @param string $message
*/
public function debug($message, array $context = [])
{
$this->log(Logger::DEBUG, $message, $context);
}
}
-199
View File
@@ -1,199 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Logger;
use Mustache\Exception\InvalidArgumentException;
use Mustache\Exception\LogicException;
use Mustache\Exception\RuntimeException;
use Mustache\Logger;
/**
* A Mustache Stream Logger.
*
* The Stream Logger wraps a file resource instance (such as a stream) or a
* stream URL. All log messages over the threshold level will be appended to
* this stream.
*
* Hint: Try `php://stderr` for your stream URL.
*/
class StreamLogger extends AbstractLogger
{
protected static $levels = [
self::DEBUG => 100,
self::INFO => 200,
self::NOTICE => 250,
self::WARNING => 300,
self::ERROR => 400,
self::CRITICAL => 500,
self::ALERT => 550,
self::EMERGENCY => 600,
];
protected $level;
protected $stream = null;
protected $url = null;
/**
* @throws InvalidArgumentException if the logging level is unknown
*
* @param resource|string $stream Resource instance or URL
* @param int $level The minimum logging level at which this handler will be triggered
*/
public function __construct($stream, $level = Logger::ERROR)
{
$this->setLevel($level);
if (is_resource($stream)) {
$this->stream = $stream;
} else {
$this->url = $stream;
}
}
/**
* Close stream resources.
*/
public function __destruct()
{
if (is_resource($this->stream)) {
fclose($this->stream);
}
}
/**
* Set the minimum logging level.
*
* @throws InvalidArgumentException if the logging level is unknown
*
* @param int $level The minimum logging level which will be written
*/
public function setLevel($level)
{
if (!array_key_exists($level, self::$levels)) {
throw new InvalidArgumentException(sprintf('Unexpected logging level: %s', $level));
}
$this->level = $level;
}
/**
* Get the current minimum logging level.
*
* @return int
*/
public function getLevel()
{
return $this->level;
}
/**
* Logs with an arbitrary level.
*
* @throws InvalidArgumentException if the logging level is unknown
*
* @param mixed $level
* @param string $message
*/
public function log($level, $message, array $context = [])
{
if (!array_key_exists($level, self::$levels)) {
throw new InvalidArgumentException(sprintf('Unexpected logging level: %s', $level));
}
if (self::$levels[$level] >= self::$levels[$this->level]) {
$this->writeLog($level, $message, $context);
}
}
/**
* Write a record to the log.
*
* @throws LogicException If neither a stream resource nor url is present
* @throws RuntimeException If the stream url cannot be opened
*
* @param int $level The logging level
* @param string $message The log message
* @param array $context The log context
*/
protected function writeLog($level, $message, array $context = [])
{
if (!is_resource($this->stream)) {
if (!isset($this->url)) {
throw new LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().');
}
$this->stream = fopen($this->url, 'a');
if (!is_resource($this->stream)) {
// @codeCoverageIgnoreStart
throw new RuntimeException(sprintf('The stream or file "%s" could not be opened.', $this->url));
// @codeCoverageIgnoreEnd
}
}
fwrite($this->stream, self::formatLine($level, $message, $context));
}
/**
* Gets the name of the logging level.
*
* @throws InvalidArgumentException if the logging level is unknown
*
* @param int $level
*
* @return string
*/
protected static function getLevelName($level)
{
return strtoupper($level);
}
/**
* Format a log line for output.
*
* @param int $level The logging level
* @param string $message The log message
* @param array $context The log context
*
* @return string
*/
protected static function formatLine($level, $message, array $context = [])
{
return sprintf(
"%s: %s\n",
self::getLevelName($level),
self::interpolateMessage($message, $context)
);
}
/**
* Interpolate context values into the message placeholders.
*
* @param string $message
*
* @return string
*/
protected static function interpolateMessage($message, array $context = [])
{
if (strpos($message, '{') === false) {
return $message;
}
// build a replacement array with braces around the context keys
$replace = [];
foreach ($context as $key => $val) {
$replace['{' . $key . '}'] = $val;
}
// interpolate replacement values into the the message and return
return strtr($message, $replace);
}
}
-392
View File
@@ -1,392 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
use Mustache\Exception\SyntaxException;
/**
* Mustache Parser class.
*
* This class is responsible for turning a set of Mustache tokens into a parse tree.
*/
class Parser
{
private $lineNum;
private $lineTokens;
private $pragmas;
private $defaultPragmas = [];
// Optional Mustache specs
private $dynamicNames = true;
private $inheritance = true;
private $pragmaFilters;
/**
* Process an array of Mustache tokens and convert them into a parse tree.
*
* @param array $tokens Set of Mustache tokens
*
* @return array Mustache token parse tree
*/
public function parse(array $tokens = [])
{
$this->lineNum = -1;
$this->lineTokens = 0;
$this->pragmas = $this->defaultPragmas;
$this->pragmaFilters = isset($this->pragmas[Engine::PRAGMA_FILTERS]);
return $this->buildTree($tokens);
}
/**
* Disable optional Mustache specs.
*
* @internal Users should set options in Mustache\Engine, not here :)
*
* @param bool[] $options
*/
public function setOptions(array $options)
{
if (isset($options['dynamic_names'])) {
$this->dynamicNames = $options['dynamic_names'] !== false;
}
if (isset($options['inheritance'])) {
$this->inheritance = $options['inheritance'] !== false;
}
}
/**
* Enable pragmas across all templates, regardless of the presence of pragma
* tags in the individual templates.
*
* @internal Users should set global pragmas in Mustache\Engine, not here :)
*
* @param string[] $pragmas
*/
public function setPragmas(array $pragmas)
{
$this->pragmas = [];
foreach ($pragmas as $pragma) {
$this->enablePragma($pragma);
}
$this->defaultPragmas = $this->pragmas;
}
/**
* Helper method for recursively building a parse tree.
*
* @throws SyntaxException when nesting errors or mismatched section tags are encountered
*
* @param array &$tokens Set of Mustache tokens
* @param array $parent Parent token (default: null)
*
* @return array Mustache Token parse tree
*/
private function buildTree(array &$tokens, $parent = null)
{
$nodes = [];
while (!empty($tokens)) {
$token = array_shift($tokens);
if ($token[Tokenizer::LINE] === $this->lineNum) {
$this->lineTokens++;
} else {
$this->lineNum = $token[Tokenizer::LINE];
$this->lineTokens = 0;
}
if ($token[Tokenizer::TYPE] !== Tokenizer::T_COMMENT) {
if (isset($token[Tokenizer::NAME])) {
list($name, $isDynamic) = $this->getDynamicName($token);
if ($isDynamic) {
$token[Tokenizer::NAME] = $name;
$token[Tokenizer::DYNAMIC] = true;
}
}
if ($this->pragmaFilters && isset($token[Tokenizer::NAME])) {
list($name, $filters) = $this->getNameAndFilters($token[Tokenizer::NAME]);
if (!empty($filters)) {
$token[Tokenizer::NAME] = $name;
$token[Tokenizer::FILTERS] = $filters;
}
}
}
switch ($token[Tokenizer::TYPE]) {
case Tokenizer::T_DELIM_CHANGE:
$this->checkIfTokenIsAllowedInParent($parent, $token);
$this->clearStandaloneLines($nodes, $tokens);
break;
case Tokenizer::T_SECTION:
case Tokenizer::T_INVERTED:
$this->checkIfTokenIsAllowedInParent($parent, $token);
$this->clearStandaloneLines($nodes, $tokens);
$nodes[] = $this->buildTree($tokens, $token);
break;
case Tokenizer::T_END_SECTION:
if (!isset($parent)) {
$msg = sprintf(
'Unexpected closing tag: /%s on line %d',
$token[Tokenizer::NAME],
$token[Tokenizer::LINE]
);
throw new SyntaxException($msg, $token);
}
$sameName = $token[Tokenizer::NAME] !== $parent[Tokenizer::NAME];
$tokenDynamic = isset($token[Tokenizer::DYNAMIC]) && $token[Tokenizer::DYNAMIC];
$parentDynamic = isset($parent[Tokenizer::DYNAMIC]) && $parent[Tokenizer::DYNAMIC];
if ($sameName || ($tokenDynamic !== $parentDynamic)) {
$msg = sprintf(
'Nesting error: %s%s (on line %d) vs. %s%s (on line %d)',
$parentDynamic ? '*' : '',
$parent[Tokenizer::NAME],
$parent[Tokenizer::LINE],
$tokenDynamic ? '*' : '',
$token[Tokenizer::NAME],
$token[Tokenizer::LINE]
);
throw new SyntaxException($msg, $token);
}
$this->clearStandaloneLines($nodes, $tokens);
$parent[Tokenizer::END] = $token[Tokenizer::INDEX];
$parent[Tokenizer::NODES] = $nodes;
return $parent;
case Tokenizer::T_PARTIAL:
$this->checkIfTokenIsAllowedInParent($parent, $token);
//store the whitespace prefix for laters!
if ($indent = $this->clearStandaloneLines($nodes, $tokens)) {
$token[Tokenizer::INDENT] = $indent[Tokenizer::VALUE];
}
$nodes[] = $token;
break;
case Tokenizer::T_PARENT:
$this->checkIfTokenIsAllowedInParent($parent, $token);
$nodes[] = $this->buildTree($tokens, $token);
break;
case Tokenizer::T_BLOCK_VAR:
if ($this->inheritance) {
if (isset($parent) && $parent[Tokenizer::TYPE] === Tokenizer::T_PARENT) {
$token[Tokenizer::TYPE] = Tokenizer::T_BLOCK_ARG;
}
$this->clearStandaloneLines($nodes, $tokens);
$nodes[] = $this->buildTree($tokens, $token);
} else {
// pretend this was just a normal "escaped" token...
$token[Tokenizer::TYPE] = Tokenizer::T_ESCAPED;
// TODO: figure out how to figure out if there was a space after this dollar:
$token[Tokenizer::NAME] = '$' . $token[Tokenizer::NAME];
$nodes[] = $token;
}
break;
case Tokenizer::T_PRAGMA:
$this->enablePragma($token[Tokenizer::NAME]);
// no break
case Tokenizer::T_COMMENT:
$this->clearStandaloneLines($nodes, $tokens);
$nodes[] = $token;
break;
default:
$nodes[] = $token;
break;
}
}
if (isset($parent)) {
$msg = sprintf(
'Missing closing tag: %s opened on line %d',
$parent[Tokenizer::NAME],
$parent[Tokenizer::LINE]
);
throw new SyntaxException($msg, $parent);
}
return $nodes;
}
/**
* Clear standalone line tokens.
*
* Returns a whitespace token for indenting partials, if applicable.
*
* @param array $nodes Parsed nodes
* @param array $tokens Tokens to be parsed
*
* @return array|null Resulting indent token, if any
*/
private function clearStandaloneLines(array &$nodes, array &$tokens)
{
if ($this->lineTokens > 1) {
// this is the third or later node on this line, so it can't be standalone
return;
}
$prev = null;
if ($this->lineTokens === 1) {
// this is the second node on this line, so it can't be standalone
// unless the previous node is whitespace.
if ($prev = end($nodes)) {
if (!$this->tokenIsWhitespace($prev)) {
return;
}
}
}
if ($next = reset($tokens)) {
// If we're on a new line, bail.
if ($next[Tokenizer::LINE] !== $this->lineNum) {
return;
}
// If the next token isn't whitespace, bail.
if (!$this->tokenIsWhitespace($next)) {
return;
}
if (count($tokens) !== 1) {
// Unless it's the last token in the template, the next token
// must end in newline for this to be standalone.
if (substr($next[Tokenizer::VALUE], -1) !== "\n") {
return;
}
}
// Discard the whitespace suffix
array_shift($tokens);
}
if ($prev) {
// Return the whitespace prefix, if any
return array_pop($nodes);
}
}
/**
* Check whether token is a whitespace token.
*
* True if token type is T_TEXT and value is all whitespace characters.
*
* @return bool True if token is a whitespace token
*/
private function tokenIsWhitespace(array $token)
{
if ($token[Tokenizer::TYPE] === Tokenizer::T_TEXT) {
return preg_match('/^\s*$/', $token[Tokenizer::VALUE]);
}
return false;
}
/**
* Check whether a token is allowed inside a parent tag.
*
* @throws SyntaxException if an invalid token is found inside a parent tag
*
* @param array|null $parent
*/
private function checkIfTokenIsAllowedInParent($parent, array $token)
{
if (isset($parent) && $parent[Tokenizer::TYPE] === Tokenizer::T_PARENT) {
throw new SyntaxException('Illegal content in < parent tag', $token);
}
}
/**
* Parse dynamic names.
*
* @throws SyntaxException when a tag does not allow *
* @throws SyntaxException on multiple *s, or dots or filters with *
*/
private function getDynamicName(array $token)
{
$name = $token[Tokenizer::NAME];
$isDynamic = false;
if ($this->dynamicNames && preg_match('/^\s*\*\s*/', $name)) {
$this->ensureTagAllowsDynamicNames($token);
$name = preg_replace('/^\s*\*\s*/', '', $name);
$isDynamic = true;
}
return [$name, $isDynamic];
}
/**
* Check whether the given token supports dynamic tag names.
*
* @throws SyntaxException when a tag does not allow *
*/
private function ensureTagAllowsDynamicNames(array $token)
{
switch ($token[Tokenizer::TYPE]) {
case Tokenizer::T_PARTIAL:
case Tokenizer::T_PARENT:
case Tokenizer::T_END_SECTION:
return;
}
$msg = sprintf(
'Invalid dynamic name: %s in %s tag',
$token[Tokenizer::NAME],
Tokenizer::getTagName($token[Tokenizer::TYPE])
);
throw new SyntaxException($msg, $token);
}
/**
* Split a tag name into name and filters.
*
* @param string $name
*
* @return array [Tag name, Array of filters]
*/
private function getNameAndFilters($name)
{
$filters = array_map('trim', explode('|', $name));
$name = array_shift($filters);
return [$name, $filters];
}
/**
* Enable a pragma.
*
* @param string $name
*/
private function enablePragma($name)
{
$this->pragmas[$name] = true;
switch ($name) {
case Engine::PRAGMA_FILTERS:
$this->pragmaFilters = true;
break;
}
}
}
-51
View File
@@ -1,51 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
/**
* A class representing a rendered string in Mustache.
*
* This is primarily used to prevent re-rendering of strings that have already
* been processed in higher-order sections.
*
* @see LambdaHelper::render()
* @see LambdaHelper::preventRender()
*/
class RenderedString
{
private $value;
/**
* RenderedString constructor.
*
* @param string $value The rendered string value
*/
public function __construct($value)
{
$this->value = (string) $value;
}
public function __toString()
{
return $this->value;
}
/**
* Get the rendered string value.
*
* @return string The rendered string value
*/
public function getValue()
{
return $this->value;
}
}
-39
View File
@@ -1,39 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
interface Source
{
/**
* Get the Source key (used to generate the compiled class name).
*
* This must return a distinct key for each template source. For example, an
* MD5 hash of the template contents would probably do the trick. The
* ProductionFilesystemLoader uses mtime and file path. If your production
* source directory is under version control, you could use the current Git
* rev and the file path...
*
* @throws RuntimeException when a source file cannot be read
*
* @return string
*/
public function getKey();
/**
* Get the template Source.
*
* @throws RuntimeException when a source file cannot be read
*
* @return string
*/
public function getSource();
}
@@ -1,81 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache\Source;
use Mustache\Exception\RuntimeException;
use Mustache\Source;
/**
* Mustache template Filesystem Source.
*
* This template Source uses stat() to generate the Source key, so that using
* pre-compiled templates doesn't require hitting the disk to read the source.
* It is more suitable for production use, and is used by default in the
* ProductionFilesystemLoader.
*/
class FilesystemSource implements Source
{
private $fileName;
private $statProps;
private $stat;
/**
* Filesystem Source constructor.
*
* @param string $fileName
*/
public function __construct($fileName, array $statProps)
{
$this->fileName = $fileName;
$this->statProps = $statProps;
}
/**
* Get the Source key (used to generate the compiled class name).
*
* @throws RuntimeException when a source file cannot be read
*
* @return string
*/
public function getKey()
{
$chunks = [
'fileName' => $this->fileName,
];
if (!empty($this->statProps)) {
if (!isset($this->stat)) {
$this->stat = @stat($this->fileName);
}
if ($this->stat === false) {
throw new RuntimeException(sprintf('Failed to read source file "%s".', $this->fileName));
}
foreach ($this->statProps as $prop) {
$chunks[$prop] = $this->stat[$prop];
}
}
return json_encode($chunks);
}
/**
* Get the template Source.
*
* @return string
*/
public function getSource()
{
return file_get_contents($this->fileName);
}
}
-193
View File
@@ -1,193 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
/**
* Abstract Mustache Template class.
*
* @abstract
*/
abstract class Template
{
/**
* @var Engine
*/
protected $mustache;
/**
* @var bool
*/
protected $strictCallables = false;
/**
* @var bool
*/
protected $lambdas = true;
/**
* Mustache Template constructor.
*/
public function __construct(Engine $mustache)
{
$this->mustache = $mustache;
}
/**
* Mustache Template instances can be treated as a function and rendered by simply calling them.
*
* $m = new \Mustache\Engine;
* $tpl = $m->loadTemplate('Hello, {{ name }}!');
* echo $tpl(['name' => 'World']); // "Hello, World!"
*
* @see \Mustache\Template::render
*
* @param mixed $context Array or object rendering context (default: [])
*
* @return string Rendered template
*/
public function __invoke($context = [])
{
return $this->render($context);
}
/**
* Render this template given the rendering context.
*
* @param mixed $context Array or object rendering context (default: [])
*
* @return string Rendered template
*/
public function render($context = [])
{
return $this->renderInternal(
$this->prepareContextStack($context)
);
}
/**
* Internal rendering method implemented by Mustache Template concrete subclasses.
*
* This is where the magic happens :)
*
* NOTE: This method is not part of the Mustache.php public API.
*
* @param string $indent (default: '')
*
* @return string Rendered template
*/
abstract public function renderInternal(Context $context, $indent = '');
/**
* Tests whether a value should be iterated over (e.g. in a section context).
*
* In most languages there are two distinct array types: list and hash (or whatever you want to call them). Lists
* should be iterated, hashes should be treated as objects. Mustache follows this paradigm for Ruby, Javascript,
* Java, Python, etc.
*
* PHP, however, treats lists and hashes as one primitive type: array. So Mustache.php needs a way to distinguish
* between between a list of things (numeric, normalized array) and a set of variables to be used as section context
* (associative array). In other words, this will be iterated over:
*
* $items = [
* ['name' => 'foo'],
* ['name' => 'bar'],
* ['name' => 'baz'],
* ];
*
* ... but this will be used as a section context block:
*
* $items = [
* 1 => ['name' => 'foo'],
* 'banana' => ['name' => 'bar'],
* 42 => ['name' => 'baz'],
* ];
*
* @param mixed $value
*
* @return bool True if the value is 'iterable'
*/
protected function isIterable($value)
{
switch (gettype($value)) {
case 'object':
return $value instanceof \Traversable;
case 'array':
$i = 0;
foreach ($value as $k => $v) {
if ($k !== $i++) {
return false;
}
}
return true;
default:
return false;
}
}
/**
* Helper method to prepare the Context stack.
*
* Adds the Mustache HelperCollection to the stack's top context frame if helpers are present.
*
* @param mixed $context Optional first context frame (default: null)
*
* @return Context
*/
protected function prepareContextStack($context = null)
{
$stack = new Context(null, $this->mustache->getBuggyPropertyShadowing());
$helpers = $this->mustache->getHelpers();
if (!$helpers->isEmpty()) {
$stack->push($helpers);
}
if (!empty($context)) {
$stack->push($context);
}
return $stack;
}
/**
* Resolve a context value.
*
* Invoke the value if it is callable, otherwise return the value.
*
* @param mixed $value
*
* @return string
*/
protected function resolveValue($value, Context $context)
{
if (!$this->lambdas) {
return $value;
}
if (($this->strictCallables ? is_object($value) : !is_string($value)) && is_callable($value)) {
$result = call_user_func($value);
if (is_string($result)) {
return $this->mustache
->loadLambda($result)
->renderInternal($context);
}
return $result;
}
return $value;
}
}
-412
View File
@@ -1,412 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mustache;
use Mustache\Exception\InvalidArgumentException;
use Mustache\Exception\SyntaxException;
/**
* Mustache Tokenizer class.
*
* This class is responsible for turning raw template source into a set of Mustache tokens.
*/
class Tokenizer
{
// Finite state machine states
const IN_TEXT = 0;
const IN_TAG_TYPE = 1;
const IN_TAG = 2;
// Token types
const T_SECTION = '#';
const T_INVERTED = '^';
const T_END_SECTION = '/';
const T_COMMENT = '!';
const T_PARTIAL = '>';
const T_PARENT = '<';
const T_DELIM_CHANGE = '=';
const T_ESCAPED = '_v';
const T_UNESCAPED = '{';
const T_UNESCAPED_2 = '&';
const T_TEXT = '_t';
const T_PRAGMA = '%';
const T_BLOCK_VAR = '$';
const T_BLOCK_ARG = '$arg';
// Valid token types
private static $tagTypes = [
self::T_SECTION => true,
self::T_INVERTED => true,
self::T_END_SECTION => true,
self::T_COMMENT => true,
self::T_PARTIAL => true,
self::T_PARENT => true,
self::T_DELIM_CHANGE => true,
self::T_ESCAPED => true,
self::T_UNESCAPED => true,
self::T_UNESCAPED_2 => true,
self::T_PRAGMA => true,
self::T_BLOCK_VAR => true,
];
private static $tagNames = [
self::T_SECTION => 'section',
self::T_INVERTED => 'inverted section',
self::T_END_SECTION => 'section end',
self::T_COMMENT => 'comment',
self::T_PARTIAL => 'partial',
self::T_PARENT => 'parent',
self::T_DELIM_CHANGE => 'set delimiter',
self::T_ESCAPED => 'variable',
self::T_UNESCAPED => 'unescaped variable',
self::T_UNESCAPED_2 => 'unescaped variable',
self::T_PRAGMA => 'pragma',
self::T_BLOCK_VAR => 'block variable',
self::T_BLOCK_ARG => 'block variable',
];
// Token properties
const TYPE = 'type';
const NAME = 'name';
const DYNAMIC = 'dynamic';
const OTAG = 'otag';
const CTAG = 'ctag';
const LINE = 'line';
const INDEX = 'index';
const END = 'end';
const INDENT = 'indent';
const NODES = 'nodes';
const VALUE = 'value';
const FILTERS = 'filters';
private $state;
private $tagType;
private $buffer;
private $tokens;
private $seenTag;
private $line;
private $otag;
private $otagChar;
private $otagLen;
private $ctag;
private $ctagChar;
private $ctagLen;
/**
* Scan and tokenize template source.
*
* @throws SyntaxException when mismatched section tags are encountered
* @throws InvalidArgumentException when $delimiters string is invalid
*
* @param string $text Mustache template source to tokenize
* @param string $delimiters Optionally, pass initial opening and closing delimiters (default: empty string)
*
* @return array Set of Mustache tokens
*/
public function scan($text, $delimiters = '')
{
// Setting mbstring.func_overload makes things *really* slow.
// Let's do everyone a favor and scan this string as ASCII instead.
//
// The INI directive was removed in PHP 8.0 so we don't need to check there (and can drop it
// when we remove support for older versions of PHP).
//
// @codeCoverageIgnoreStart
$encoding = null;
if (version_compare(PHP_VERSION, '8.0.0', '<')) {
if (function_exists('mb_internal_encoding') && ini_get('mbstring.func_overload') & 2) {
$encoding = mb_internal_encoding();
mb_internal_encoding('ASCII');
}
}
// @codeCoverageIgnoreEnd
$this->reset();
if (is_string($delimiters) && ($delimiters = trim($delimiters)) !== '') {
$this->setDelimiters($delimiters);
}
$len = strlen($text);
for ($i = 0; $i < $len; $i++) {
switch ($this->state) {
case self::IN_TEXT:
$char = $text[$i];
// Test whether it's time to change tags.
if ($char === $this->otagChar && substr($text, $i, $this->otagLen) === $this->otag) {
$i--;
$this->flushBuffer();
$this->state = self::IN_TAG_TYPE;
} else {
$this->buffer .= $char;
if ($char === "\n") {
$this->flushBuffer();
$this->line++;
}
}
break;
case self::IN_TAG_TYPE:
$i += $this->otagLen - 1;
$char = $text[$i + 1];
if (isset(self::$tagTypes[$char])) {
$tag = $char;
$this->tagType = $tag;
} else {
$tag = null;
$this->tagType = self::T_ESCAPED;
}
if ($this->tagType === self::T_DELIM_CHANGE) {
$i = $this->changeDelimiters($text, $i);
$this->state = self::IN_TEXT;
} elseif ($this->tagType === self::T_PRAGMA) {
$i = $this->addPragma($text, $i);
$this->state = self::IN_TEXT;
} else {
if ($tag !== null) {
$i++;
}
$this->state = self::IN_TAG;
}
$this->seenTag = $i;
break;
default:
$char = $text[$i];
// Test whether it's time to change tags.
if ($char === $this->ctagChar && substr($text, $i, $this->ctagLen) === $this->ctag) {
$token = [
self::TYPE => $this->tagType,
self::NAME => trim($this->buffer),
self::OTAG => $this->otag,
self::CTAG => $this->ctag,
self::LINE => $this->line,
self::INDEX => ($this->tagType === self::T_END_SECTION) ? $this->seenTag - $this->otagLen : $i + $this->ctagLen,
];
if ($this->tagType === self::T_UNESCAPED) {
// Clean up `{{{ tripleStache }}}` style tokens.
if ($this->ctag === '}}') {
if (($i + 2 < $len) && $text[$i + 2] === '}') {
$i++;
} else {
$msg = sprintf(
'Mismatched tag delimiters: %s on line %d',
$token[self::NAME],
$token[self::LINE]
);
throw new SyntaxException($msg, $token);
}
} else {
$lastName = $token[self::NAME];
if (substr($lastName, -1) === '}') {
$token[self::NAME] = trim(substr($lastName, 0, -1));
} else {
$msg = sprintf(
'Mismatched tag delimiters: %s on line %d',
$token[self::NAME],
$token[self::LINE]
);
throw new SyntaxException($msg, $token);
}
}
}
$this->buffer = '';
$i += $this->ctagLen - 1;
$this->state = self::IN_TEXT;
$this->tokens[] = $token;
} else {
$this->buffer .= $char;
}
break;
}
}
if ($this->state !== self::IN_TEXT) {
$this->throwUnclosedTagException();
}
$this->flushBuffer();
// Restore the user's encoding...
// @codeCoverageIgnoreStart
if ($encoding) {
mb_internal_encoding($encoding);
}
// @codeCoverageIgnoreEnd
return $this->tokens;
}
/**
* Helper function to reset tokenizer internal state.
*/
private function reset()
{
$this->state = self::IN_TEXT;
$this->tagType = null;
$this->buffer = '';
$this->tokens = [];
$this->seenTag = false;
$this->line = 0;
$this->otag = '{{';
$this->otagChar = '{';
$this->otagLen = 2;
$this->ctag = '}}';
$this->ctagChar = '}';
$this->ctagLen = 2;
}
/**
* Flush the current buffer to a token.
*/
private function flushBuffer()
{
if (strlen($this->buffer) > 0) {
$this->tokens[] = [
self::TYPE => self::T_TEXT,
self::LINE => $this->line,
self::VALUE => $this->buffer,
];
$this->buffer = '';
}
}
/**
* Change the current Mustache delimiters. Set new `otag` and `ctag` values.
*
* @throws SyntaxException when delimiter string is invalid
*
* @param string $text Mustache template source
* @param int $index Current tokenizer index
*
* @return int New index value
*/
private function changeDelimiters($text, $index)
{
$startIndex = strpos($text, '=', $index) + 1;
$close = '=' . $this->ctag;
$closeIndex = strpos($text, $close, $index);
if ($closeIndex === false) {
$this->throwUnclosedTagException();
}
$token = [
self::TYPE => self::T_DELIM_CHANGE,
self::LINE => $this->line,
];
try {
$this->setDelimiters(trim(substr($text, $startIndex, $closeIndex - $startIndex)));
} catch (InvalidArgumentException $e) {
throw new SyntaxException($e->getMessage(), $token);
}
$this->tokens[] = $token;
return $closeIndex + strlen($close) - 1;
}
/**
* Set the current Mustache `otag` and `ctag` delimiters.
*
* @throws InvalidArgumentException when delimiter string is invalid
*
* @param string $delimiters
*/
private function setDelimiters($delimiters)
{
if (!preg_match('/^\s*(\S+)\s+(\S+)\s*$/', $delimiters, $matches)) {
throw new InvalidArgumentException(sprintf('Invalid delimiters: %s', $delimiters));
}
list($_, $otag, $ctag) = $matches;
$this->otag = $otag;
$this->otagChar = $otag[0];
$this->otagLen = strlen($otag);
$this->ctag = $ctag;
$this->ctagChar = $ctag[0];
$this->ctagLen = strlen($ctag);
}
/**
* Add pragma token.
*
* Pragmas are hoisted to the front of the template, so all pragma tokens
* will appear at the front of the token list.
*
* @param string $text
* @param int $index
*
* @return int New index value
*/
private function addPragma($text, $index)
{
$end = strpos($text, $this->ctag, $index);
if ($end === false) {
$this->throwUnclosedTagException();
}
$pragma = trim(substr($text, $index + 2, $end - $index - 2));
// Pragmas are hoisted to the front of the template.
array_unshift($this->tokens, [
self::TYPE => self::T_PRAGMA,
self::NAME => $pragma,
self::LINE => 0,
]);
return $end + $this->ctagLen - 1;
}
private function throwUnclosedTagException()
{
$name = trim($this->buffer);
if ($name !== '') {
$msg = sprintf('Unclosed tag: %s on line %d', $name, $this->line);
} else {
$msg = sprintf('Unclosed tag on line %d', $this->line);
}
throw new SyntaxException($msg, [
self::TYPE => $this->tagType,
self::NAME => $name,
self::OTAG => $this->otag,
self::CTAG => $this->ctag,
self::LINE => $this->line,
self::INDEX => $this->seenTag - $this->otagLen,
]);
}
/**
* Get the human readable name for a tag type.
*
* @param string $tagType One of the tokenizer T_* constants
*
* @return string
*/
public static function getTagName($tagType)
{
return isset(self::$tagNames[$tagType]) ? self::$tagNames[$tagType] : 'unknown';
}
}
-282
View File
@@ -1,282 +0,0 @@
<?php
/*
* This file is part of Mustache.php.
*
* (c) 2010-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
class_alias(\Mustache\Cache::class, \Mustache_Cache::class);
class_alias(\Mustache\Cache\AbstractCache::class, \Mustache_Cache_AbstractCache::class);
class_alias(\Mustache\Cache\FilesystemCache::class, \Mustache_Cache_FilesystemCache::class);
class_alias(\Mustache\Cache\NoopCache::class, \Mustache_Cache_NoopCache::class);
class_alias(\Mustache\Compiler::class, \Mustache_Compiler::class);
class_alias(\Mustache\Context::class, \Mustache_Context::class);
class_alias(\Mustache\Engine::class, \Mustache_Engine::class);
class_alias(\Mustache\Exception::class, \Mustache_Exception::class);
class_alias(\Mustache\Exception\InvalidArgumentException::class, \Mustache_Exception_InvalidArgumentException::class);
class_alias(\Mustache\Exception\LogicException::class, \Mustache_Exception_LogicException::class);
class_alias(\Mustache\Exception\RuntimeException::class, \Mustache_Exception_RuntimeException::class);
class_alias(\Mustache\Exception\SyntaxException::class, \Mustache_Exception_SyntaxException::class);
class_alias(\Mustache\Exception\UnknownFilterException::class, \Mustache_Exception_UnknownFilterException::class);
class_alias(\Mustache\Exception\UnknownHelperException::class, \Mustache_Exception_UnknownHelperException::class);
class_alias(\Mustache\Exception\UnknownTemplateException::class, \Mustache_Exception_UnknownTemplateException::class);
class_alias(\Mustache\HelperCollection::class, \Mustache_HelperCollection::class);
class_alias(\Mustache\LambdaHelper::class, \Mustache_LambdaHelper::class);
class_alias(\Mustache\Loader::class, \Mustache_Loader::class);
class_alias(\Mustache\Loader\ArrayLoader::class, \Mustache_Loader_ArrayLoader::class);
class_alias(\Mustache\Loader\CascadingLoader::class, \Mustache_Loader_CascadingLoader::class);
class_alias(\Mustache\Loader\FilesystemLoader::class, \Mustache_Loader_FilesystemLoader::class);
class_alias(\Mustache\Loader\InlineLoader::class, \Mustache_Loader_InlineLoader::class);
class_alias(\Mustache\Loader\MutableLoader::class, \Mustache_Loader_MutableLoader::class);
class_alias(\Mustache\Loader\ProductionFilesystemLoader::class, \Mustache_Loader_ProductionFilesystemLoader::class);
class_alias(\Mustache\Loader\StringLoader::class, \Mustache_Loader_StringLoader::class);
class_alias(\Mustache\Logger::class, \Mustache_Logger::class);
class_alias(\Mustache\Logger\AbstractLogger::class, \Mustache_Logger_AbstractLogger::class);
class_alias(\Mustache\Logger\StreamLogger::class, \Mustache_Logger_StreamLogger::class);
class_alias(\Mustache\Parser::class, \Mustache_Parser::class);
class_alias(\Mustache\Source::class, \Mustache_Source::class);
class_alias(\Mustache\Source\FilesystemSource::class, \Mustache_Source_FilesystemSource::class);
class_alias(\Mustache\Template::class, \Mustache_Template::class);
class_alias(\Mustache\Tokenizer::class, \Mustache_Tokenizer::class);
if (!class_exists(\Mustache_Engine::class)) {
/** @deprecated use Mustache\Engine */
class Mustache_Engine extends \Mustache\Engine
{
}
}
if (!interface_exists(\Mustache_Cache::class)) {
/** @deprecated use Mustache\Cache */
interface Mustache_Cache extends \Mustache\Cache
{
}
}
if (!class_exists(\Mustache_Cache_AbstractCache::class)) {
/** @deprecated use Mustache\Cache\AbstractCache */
abstract class Mustache_Cache_AbstractCache extends \Mustache\Cache\AbstractCache
{
}
}
if (!class_exists(\Mustache_Cache_FilesystemCache::class)) {
/** @deprecated use Mustache\Cache\FilesystemCache */
class Mustache_Cache_FilesystemCache extends \Mustache\Cache\FilesystemCache
{
}
}
if (!class_exists(\Mustache_Cache_NoopCache::class)) {
/** @deprecated use Mustache\Cache\NoopCache */
class Mustache_Cache_NoopCache extends \Mustache\Cache\NoopCache
{
}
}
if (!class_exists(\Mustache_Compiler::class)) {
/** @deprecated use Mustache\Compiler */
class Mustache_Compiler extends \Mustache\Compiler
{
}
}
if (!class_exists(\Mustache_Context::class)) {
/** @deprecated use Mustache\Context */
class Mustache_Context extends \Mustache\Context
{
}
}
if (!class_exists(\Mustache_Engine::class)) {
/** @deprecated use Mustache\Engine */
class Mustache_Engine extends \Mustache\Engine
{
}
}
if (!interface_exists(\Mustache_Exception::class)) {
/** @deprecated use Mustache\Exception */
interface Mustache_Exception extends \Mustache\Exception
{
}
}
if (!class_exists(\Mustache_Exception_InvalidArgumentException::class)) {
/** @deprecated use Mustache\Exception\InvalidArgumentException */
class Mustache_Exception_InvalidArgumentException extends \Mustache\Exception\InvalidArgumentException
{
}
}
if (!class_exists(\Mustache_Exception_LogicException::class)) {
/** @deprecated use Mustache\Exception\LogicException */
class Mustache_Exception_LogicException extends \Mustache\Exception\LogicException
{
}
}
if (!class_exists(\Mustache_Exception_RuntimeException::class)) {
/** @deprecated use Mustache\Exception\RuntimeException */
class Mustache_Exception_RuntimeException extends \Mustache\Exception\RuntimeException
{
}
}
if (!class_exists(\Mustache_Exception_SyntaxException::class)) {
/** @deprecated use Mustache\Exception\SyntaxException */
class Mustache_Exception_SyntaxException extends \Mustache\Exception\SyntaxException
{
}
}
if (!class_exists(\Mustache_Exception_UnknownFilterException::class)) {
/** @deprecated use Mustache\Exception\UnknownFilterException */
class Mustache_Exception_UnknownFilterException extends \Mustache\Exception\UnknownFilterException
{
}
}
if (!class_exists(\Mustache_Exception_UnknownHelperException::class)) {
/** @deprecated use Mustache\Exception\UnknownHelperException */
class Mustache_Exception_UnknownHelperException extends \Mustache\Exception\UnknownHelperException
{
}
}
if (!class_exists(\Mustache_Exception_UnknownTemplateException::class)) {
/** @deprecated use Mustache\Exception\UnknownTemplateException */
class Mustache_Exception_UnknownTemplateException extends \Mustache\Exception\UnknownTemplateException
{
}
}
if (!class_exists(\Mustache_HelperCollection::class)) {
/** @deprecated use Mustache\HelperCollection */
class Mustache_HelperCollection extends \Mustache\HelperCollection
{
}
}
if (!class_exists(\Mustache_LambdaHelper::class)) {
/** @deprecated use Mustache\LambdaHelper */
class Mustache_LambdaHelper extends \Mustache\LambdaHelper
{
}
}
if (!interface_exists(\Mustache_Loader::class)) {
/** @deprecated use Mustache\Loader */
interface Mustache_Loader extends \Mustache\Loader
{
}
}
if (!class_exists(\Mustache_Loader_ArrayLoader::class)) {
/** @deprecated use Mustache\Loader\ArrayLoader */
class Mustache_Loader_ArrayLoader extends \Mustache\Loader\ArrayLoader
{
}
}
if (!class_exists(\Mustache_Loader_CascadingLoader::class)) {
/** @deprecated use Mustache\Loader\CascadingLoader */
class Mustache_Loader_CascadingLoader extends \Mustache\Loader\CascadingLoader
{
}
}
if (!class_exists(\Mustache_Loader_FilesystemLoader::class)) {
/** @deprecated use Mustache\Loader\FilesystemLoader */
class Mustache_Loader_FilesystemLoader extends \Mustache\Loader\FilesystemLoader
{
}
}
if (!class_exists(\Mustache_Loader_InlineLoader::class)) {
/** @deprecated use Mustache\Loader\InlineLoader */
class Mustache_Loader_InlineLoader extends \Mustache\Loader\InlineLoader
{
}
}
if (!interface_exists(\Mustache_Loader_MutableLoader::class)) {
/** @deprecated use Mustache\Loader\MutableLoader */
interface Mustache_Loader_MutableLoader extends \Mustache\Loader\MutableLoader
{
}
}
if (!class_exists(\Mustache_Loader_ProductionFilesystemLoader::class)) {
/** @deprecated use Mustache\Loader\ProductionFilesystemLoader */
class Mustache_Loader_ProductionFilesystemLoader extends \Mustache\Loader\ProductionFilesystemLoader
{
}
}
if (!class_exists(\Mustache_Loader_StringLoader::class)) {
/** @deprecated use Mustache\Loader\StringLoader */
class Mustache_Loader_StringLoader extends \Mustache\Loader\StringLoader
{
}
}
if (!interface_exists(\Mustache_Logger::class)) {
/** @deprecated use Mustache\Logger */
interface Mustache_Logger extends \Mustache\Logger
{
}
}
if (!class_exists(\Mustache_Logger_AbstractLogger::class)) {
/** @deprecated use Mustache\Logger\AbstractLogger */
abstract class Mustache_Logger_AbstractLogger extends \Mustache\Logger\AbstractLogger
{
}
}
if (!class_exists(\Mustache_Logger_StreamLogger::class)) {
/** @deprecated use Mustache\Logger\StreamLogger */
class Mustache_Logger_StreamLogger extends \Mustache\Logger\StreamLogger
{
}
}
if (!class_exists(\Mustache_Parser::class)) {
/** @deprecated use Mustache\Parser */
class Mustache_Parser extends \Mustache\Parser
{
}
}
if (!interface_exists(\Mustache_Source::class)) {
/** @deprecated use Mustache\Source */
interface Mustache_Source extends \Mustache\Source
{
}
}
if (!class_exists(\Mustache_Source_FilesystemSource::class)) {
/** @deprecated use Mustache\Source\FilesystemSource */
class Mustache_Source_FilesystemSource extends \Mustache\Source\FilesystemSource
{
}
}
if (!class_exists(\Mustache_Template::class)) {
/** @deprecated use Mustache\Template */
abstract class Mustache_Template extends \Mustache\Template
{
}
}
if (!class_exists(\Mustache_Tokenizer::class)) {
/** @deprecated use Mustache\Tokenizer */
class Mustache_Tokenizer extends \Mustache\Tokenizer
{
}
}
File diff suppressed because it is too large Load Diff
-56
View File
@@ -1,56 +0,0 @@
<?xml version="1.0"?>
<hivemq xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/peez80/docker-hivemq/master/hivemq-config.xsd"
>
<listeners>
<!-- MQTT port without TLS -->
<tcp-listener>
<port>1883</port>
<bind-address>0.0.0.0</bind-address>
</tcp-listener>
<!-- MQTT port with TLS but without client certificate validation -->
<tls-tcp-listener>
<port>8883</port>
<bind-address>0.0.0.0</bind-address>
<tls>
<keystore>
<path>/hivemq-certs/server.jks</path>
<password>s3cr3t</password>
<private-key-password>s3cr3t</private-key-password>
</keystore>
<protocols>
<protocol>TLSv1.3</protocol>
<protocol>TLSv1.2</protocol>
<protocol>TLSv1.1</protocol>
<protocol>TLSv1</protocol>
</protocols>
</tls>
</tls-tcp-listener>
<!-- MQTT port with TLS and with client certificate validation -->
<tls-tcp-listener>
<port>8884</port>
<bind-address>0.0.0.0</bind-address>
<tls>
<client-authentication-mode>REQUIRED</client-authentication-mode>
<truststore>
<path>/hivemq-certs/ca.jks</path>
<password>s3cr3t</password>
</truststore>
<keystore>
<path>/hivemq-certs/server.jks</path>
<password>s3cr3t</password>
<private-key-password>s3cr3t</private-key-password>
</keystore>
<protocols>
<protocol>TLSv1.3</protocol>
<protocol>TLSv1.2</protocol>
<protocol>TLSv1.1</protocol>
<protocol>TLSv1</protocol>
</protocols>
</tls>
</tls-tcp-listener>
</listeners>
</hivemq>
-31
View File
@@ -1,31 +0,0 @@
# Config file for mosquitto
per_listener_settings true
# Port to use for the default listener.
listener 1883
allow_anonymous true
# Port to use for the default listener with authentication.
listener 1884
password_file /mosquitto/config/mosquitto.passwd
allow_anonymous false
# =================================================================
# Extra listeners
# =================================================================
# TLS listener without client certificate requirement
listener 8883
cafile /mosquitto-certs/ca.crt
certfile /mosquitto-certs/server.crt
keyfile /mosquitto-certs/server.key
require_certificate false
allow_anonymous true
# TLS listener with client certificate requirement
listener 8884
cafile /mosquitto-certs/ca.crt
certfile /mosquitto-certs/server.crt
keyfile /mosquitto-certs/server.key
require_certificate true
allow_anonymous true
-1
View File
@@ -1 +0,0 @@
ci-test-user:$6$QypQBNSQKE5bg6Ec$nzACfxhQ9qiYFByPPM/6GP/9kOWwDzEftN0EJPkS6M0PWqL55jAbBxUO863oWwhJ2q/YaubfLbe3xwwhBuoStQ==
-11
View File
@@ -1,11 +0,0 @@
listeners.tcp.default = 5672
loopback_users.guest = false
mqtt.listeners.tcp.default = 1883
mqtt.listeners.ssl = none
mqtt.allow_anonymous = true
mqtt.default_user = guest
mqtt.default_pass = guest
mqtt.vhost = /
mqtt.exchange = amq.topic
mqtt.subscription_ttl = 1800000
@@ -1,2 +0,0 @@
*
!.gitignore
-23
View File
@@ -1,23 +0,0 @@
version: 2
updates:
- package-ecosystem: "composer"
directory: "/"
allow:
- dependency-type: "development"
schedule:
interval: "daily"
time: "05:00"
timezone: "Europe/Vienna"
labels:
- "composer dependencies"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "Europe/Vienna"
labels:
- "github actions"
-25
View File
@@ -1,25 +0,0 @@
changelog:
exclude:
labels:
- ignore-for-release
authors:
- octocat
categories:
- title: Added
labels:
- enhancement
- title: Deprecated
labels:
- deprecated
- title: Removed
labels:
- removed
- title: Fixed
labels:
- bug
- title: Security
labels:
- security
- title: Changed
labels:
- "*"
@@ -1,23 +0,0 @@
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
pull-requests: write
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v5
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4
with:
comment-summary-in-pr: true
fail-on-scopes: 'runtime, development, unknown'
fail-on-severity: 'low'
license-check: true
vulnerability-check: true
-140
View File
@@ -1,140 +0,0 @@
name: Tests
on:
push:
branches:
- master
pull_request_target:
types: [opened, synchronize, reopened]
jobs:
test-all:
name: Test PHP ${{ matrix.php-version }} using broker [${{ matrix.mqtt-broker }}]
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['8.1', '8.2', '8.3', '8.4']
mqtt-broker: ['mosquitto-1.6', 'mosquitto-2.0', 'hivemq', 'emqx', 'rabbitmq']
include:
- php-version: '8.4'
mqtt-broker: 'mosquitto-2.0'
run-sonarqube-analysis: true
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup PHP ${{ matrix.php-version }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: phpunit:9.5.0
coverage: pcov
- name: Setup problem matchers for PHP
run: echo "::add-matcher::${{ runner.tool_cache }}/php.json"
- name: Setup problem matchers for PHPUnit
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install Composer dependencies
run: composer install --prefer-dist
- name: Generate certificates for tests
run: |
sh create-certificates.sh
chmod u+rx,g+rx ${{ github.workspace }}/.ci/tls
chmod a+r ${{ github.workspace }}/.ci/tls/*
- name: Start Mosquitto 1.6 message broker
if: matrix.mqtt-broker == 'mosquitto-1.6'
uses: Namoshek/mosquitto-github-action@v1
with:
version: '1.6'
ports: '1883:1883 1884:1884 8883:8883 8884:8884'
certificates: ${{ github.workspace }}/.ci/tls
config: ${{ github.workspace }}/.ci/mosquitto.conf
password-file: ${{ github.workspace}}/.ci/mosquitto.passwd
- name: Start Mosquitto 2.0 message broker
if: matrix.mqtt-broker == 'mosquitto-2.0'
uses: Namoshek/mosquitto-github-action@v1
with:
version: '2.0'
ports: '1883:1883 1884:1884 8883:8883 8884:8884'
certificates: ${{ github.workspace }}/.ci/tls
config: ${{ github.workspace }}/.ci/mosquitto.conf
password-file: ${{ github.workspace}}/.ci/mosquitto.passwd
- name: Start HiveMQ message broker
if: matrix.mqtt-broker == 'hivemq'
uses: Namoshek/hivemq4-github-action@v1
with:
version: '4.8.5'
ports: '1883:1883 8883:8883 8884:8884'
certificates: ${{ github.workspace }}/.ci/tls
config: ${{ github.workspace }}/.ci/hivemq.xml
- name: Start EMQ X message broker
if: matrix.mqtt-broker == 'emqx'
uses: Namoshek/emqx-github-action@v1.0.2
with:
version: '4.4.3'
ports: '1883:1883'
config: ${{ github.workspace }}/.ci/emqx.conf
- name: Start RabbitMQ message broker
if: matrix.mqtt-broker == 'rabbitmq'
uses: namoshek/rabbitmq-github-action@v1.1.0
with:
version: '3.8.9'
ports: '1883:1883'
config: ${{ github.workspace }}/.ci/rabbitmq.conf
plugins: 'rabbitmq_mqtt'
- name: Wait a bit until MQTT broker has started
run: sleep 45
- name: Run phpunit tests
run: composer test
env:
MQTT_BROKER_HOST: 'localhost'
MQTT_BROKER_PORT: 1883
MQTT_BROKER_PORT_WITH_AUTHENTICATION: ${{ (matrix.mqtt-broker == 'mosquitto-1.6' || matrix.mqtt-broker == 'mosquitto-2.0') && 1884 || 1883 }}
MQTT_BROKER_TLS_PORT: 8883
MQTT_BROKER_TLS_WITH_CLIENT_CERT_PORT: 8884
MQTT_BROKER_USERNAME: ${{ (matrix.mqtt-broker == 'mosquitto-1.6' || matrix.mqtt-broker == 'mosquitto-2.0') && 'ci-test-user' || '' }}
MQTT_BROKER_PASSWORD: ${{ (matrix.mqtt-broker == 'mosquitto-1.6' || matrix.mqtt-broker == 'mosquitto-2.0') && secrets.CI_MOSQUITTO_CI_TEST_USER_PASSWORD || '' }}
SKIP_TLS_TESTS: ${{ matrix.mqtt-broker == 'emqx' || matrix.mqtt-broker == 'rabbitmq' }}
- name: Dump Docker logs on failure
if: failure()
uses: jwalton/gh-docker-logs@v2
- name: Prepare paths for SonarQube analysis
if: matrix.run-sonarqube-analysis
run: |
sed -i "s|$GITHUB_WORKSPACE|/github/workspace|g" phpunit.coverage-clover.xml
sed -i "s|$GITHUB_WORKSPACE|/github/workspace|g" phpunit.report-junit.xml
- name: Run SonarQube analysis
uses: sonarsource/sonarqube-scan-action@v6.0.0
if: matrix.run-sonarqube-analysis
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONARCLOUD_TOKEN }}
-6
View File
@@ -1,6 +0,0 @@
.idea/
.phpunit.result.cache
composer.lock
phpunit.coverage*.xml
phpunit.report*.xml
/vendor/
-88
View File
@@ -1,88 +0,0 @@
<?xml version="1.0"?>
<ruleset name="php-mqtt Code Style Standard">
<description>php-mqtt Code Style Standard</description>
<rule ref="PSR1"/>
<rule ref="PSR2">
<exclude name="PSR2.Methods.MethodDeclaration.AbstractAfterVisibility"/>
<exclude name="Squiz.ControlStructures.ControlSignature.SpaceAfterCloseParenthesis"/>
</rule>
<rule ref="Generic.Arrays.ArrayIndent">
<exclude name="Generic.Arrays.ArrayIndent.CloseBraceNotNewLine"/>
</rule>
<rule ref="Generic.Classes.DuplicateClassName"/>
<rule ref="Generic.CodeAnalysis.EmptyStatement">
<exclude name="Generic.CodeAnalysis.EmptyStatement.DetectedCatch"/>
</rule>
<rule ref="Generic.CodeAnalysis.ForLoopShouldBeWhileLoop"/>
<rule ref="Generic.CodeAnalysis.ForLoopWithTestFunctionCall"/>
<rule ref="Generic.CodeAnalysis.JumbledIncrementer"/>
<rule ref="Generic.CodeAnalysis.UnconditionalIfStatement"/>
<rule ref="Generic.CodeAnalysis.UnnecessaryFinalModifier"/>
<rule ref="Generic.CodeAnalysis.UselessOverridingMethod"/>
<rule ref="Generic.Commenting.Todo">
<exclude-pattern>src/*</exclude-pattern>
</rule>
<rule ref="Generic.ControlStructures.InlineControlStructure"/>
<rule ref="Generic.Files.ByteOrderMark"/>
<rule ref="Generic.Files.LineEndings"/>
<rule ref="Generic.Files.LineLength">
<properties>
<property name="lineLimit" value="150"/>
<property name="absoluteLineLimit" value="0"/>
</properties>
</rule>
<rule ref="Generic.Formatting.DisallowMultipleStatements"/>
<rule ref="Generic.Formatting.MultipleStatementAlignment"/>
<rule ref="Generic.Formatting.SpaceAfterCast"/>
<rule ref="Generic.Functions.CallTimePassByReference"/>
<rule ref="Generic.Functions.FunctionCallArgumentSpacing"/>
<rule ref="Generic.Functions.OpeningFunctionBraceBsdAllman"/>
<rule ref="Generic.Metrics.CyclomaticComplexity">
<properties>
<property name="complexity" value="50"/>
<property name="absoluteComplexity" value="100"/>
</properties>
</rule>
<rule ref="Generic.Metrics.NestingLevel">
<properties>
<property name="nestingLevel" value="10"/>
<property name="absoluteNestingLevel" value="30"/>
</properties>
</rule>
<rule ref="Generic.NamingConventions.ConstructorName"/>
<rule ref="Generic.PHP.LowerCaseConstant"/>
<rule ref="Generic.PHP.DeprecatedFunctions"/>
<rule ref="Generic.PHP.DisallowShortOpenTag"/>
<rule ref="Generic.PHP.ForbiddenFunctions"/>
<rule ref="Generic.WhiteSpace.DisallowTabIndent"/>
<rule ref="Generic.WhiteSpace.ScopeIndent">
<properties>
<property name="indent" value="4"/>
</properties>
</rule>
<rule ref="MySource.PHP.EvalObjectFactory"/>
<rule ref="PEAR.Commenting.ClassComment">
<exclude name="PEAR.Commenting.ClassComment.MissingAuthorTag"/>
<exclude name="PEAR.Commenting.ClassComment.MissingCategoryTag"/>
<exclude name="PEAR.Commenting.ClassComment.MissingLicenseTag"/>
<exclude name="PEAR.Commenting.ClassComment.MissingLinkTag"/>
</rule>
<rule ref="PEAR.Commenting.ClassComment.Missing"/>
<rule ref="PEAR.Commenting.ClassComment.MissingPackageTag"/>
<rule ref="PEAR.Commenting.InlineComment"/>
<rule ref="PSR1.Classes.ClassDeclaration.MissingNamespace"/>
<rule ref="PSR2.Methods.FunctionClosingBrace.SpacingBeforeClose"/>
<rule ref="Squiz.Arrays.ArrayDeclaration.NoCommaAfterLast"/>
<rule ref="Squiz.Functions.MultiLineFunctionDeclaration.NewlineBeforeOpenBrace">
<exclude-pattern>src/*</exclude-pattern>
</rule>
<rule ref="Zend.Files.ClosingTag"/>
<file>src</file>
<arg name="colors"/>
<arg value="sp"/>
<ini name="memory_limit" value="128M"/>
</ruleset>
-72
View File
@@ -1,72 +0,0 @@
# Changelog
## Version `v1.0.0`
Significant improvements to the architecture, API and design of the library have been part of `v1.0.0`.
Upgrading should be rather simple for most users though, since the public API has not changed a lot
and only in places which are not used too frequently.
A lot of effort has been put into this summary to document as many changes as possible.
It is impossible to give a guarantee about the completeness of this list though.
You should cover your uses of the library with tests yourself as well.
The following summary compares `v0.3.0` to `v1.0.0`.
### Breaking Changes
- The library does now require PHP 7.4 and supports PHP 8.0. This move was made with the clear intention to drop support for PHP 7.4 at some point.
- The primary interface and class of the library have been renamed to StudlyCaps to follow PSR-2:
- `\PhpMqtt\Client\Contracts\MQTTClient` &rarr; `\PhpMqtt\Client\Contracts\MqttClient`
- `\PhpMqtt\Client\MQTTClient` &rarr; `\PhpMqtt\Client\MqttClient`
- `\PhpMqtt\Client\Exceptions\MQTTClientException` &rarr; `\PhpMqtt\Client\Exceptions\MqttClientException`
- Protocol specific logic has been extracted from the `MQTTClient` class to a new interface `\PhpMqtt\Client\Contracts\MessageProcessor` and the respective implementation for MQTT 3.1, `\PhpMqtt\Client\MessageProcessors\Mqtt31MessageProcessor`.
- The `MessageProcessor` is responsible for parsing and building packages on a byte level.
- Splitting the logic from the main class did not only reduce the overall complexity of the class, it also made testing a lot easier and builds a solid foundation for future development and extension by implementing more protocol versions (like MQTT 5).
- Some `protected` properties have been changed to `private` to ensure they are not manipulated outside the offered scope, which is enforced through getters and setters. This change only affects users which actively inherited their own implementation from the library.
- The QoS 2 message flow is now implemented properly and should just work.
#### Connection Settings
- The `$caFile` parameter of the `MQTTClient` constructor as well as the `$username` and `$password` parameters of the `MQTTClient::connect()` method have been moved to the `ConnectionSettings` class.
- The `ConnectionSettings` use fluent setters for configuration now ([see README](README.md)).
- The `ConnectionSettings`` passed to `MQTTClient::connect()` are now validated and may not contain invalid configuration. In case of invalid configuration, a `\PhpMqtt\Client\Exceptions\ConfigurationInvalidException` is thrown.
- Additional TLS options have been added to the `ConnectionSettings` to support more uses cases with secured connections.
#### Methods
- Most methods can now throw a `\PhpMqtt\Client\Exceptions\RepositoryException` if an interaction with the repository fails. This should happen with the `MemoryRepository` only in exceptional situations, but when implementing persisted repositories, this may happen more frequently and should therefore be considered.
- The `MQTTClient::connect()` method had a parameter called `$sendCleanSessionFlag` while the `MqttClient::connect()` method has the same parameter, but called `$useCleanSession`. The parameters `$username` and `$password` have been removed entirely and are now part of the `ConnectionSettings`.
- The method `MQTTClient::close()` has been renamed to `MqttClient::disconnect()`.
- The parameter `$topic` of `MQTTClient::subscribe()` has been renamed to `$topicFilter` to reflect its meaning (which is a topic, but with wildcards). The `$callback` parameter can be `null` now and has `null` as default.
- The parameter `$topic` of `MQTTClient::unsubscribe()` has been renamed to `$topicFilter` as well.
#### Exceptions
- New exceptions have been introduced and old ones were removed. All exceptions inherit from `\PhpMqtt\Client\Exceptions\MqttClientException` as base. You should ensure your calls to methods of the `MqttClient` handle the exceptions appropriately.
- The exception constants previously defined on the `\PhpMqtt\Client\MQTTClient` class have been moved to the respective exception classes. This change only affects you if you used these constants to render detailed exception information for your users.
#### Repositories
- The `\PhpMqtt\Client\Contracts\Repository` interface has been changed significantly and summarizing all changes would be quite hard anyway. We therefore encourage you to have a look at the interface again and update your own implementation(s) of it, if you have any.
#### Logger
- The `\PhpMqtt\Client\Logger` implementation of `Psr\Log\LoggerInterface` does now decorate the log output with details about the MQTT client (format: `MQTT [{host}:{port}] [{clientId}] {message}`).
### Additions
- It is now possible to register event handlers for received messages. In combination with subscriptions without a callback, this allows to use centralized logic for multiple subscriptions. It also can be used for centralized logging, for example.
- A lot of unit and integration tests have been added which cover most parts of the library, especially the non-exception paths.
- All unit tests, integration tests, and the code style are enforced using a GitHub Actions workflow which runs under Ubuntu against multiple MQTT brokers (currently Mosquitto, HiveMQ and EMQ X). Contributing became easier therefore, but we expect that tests are added for changes and additions.
- To run the tests locally, an MQTT broker without authorization needs to run at `localhost:1883` (or the configuration in `phpunit.xml` is changed instead).
- The project is now analyzed using [sonarcloud.io](https://sonarcloud.io/dashboard?id=php-mqtt_client) which helps us keep up the high standards of the library.
#### Methods
- `MqttClient::isConnected()`: returns `true` if a connection is established (socket opened), and `false` otherwise.
- `MqttClient::getReceivedBytes()`: returns the number of raw bytes received from the broker (this includes meta information and not only message contents).
- `MqttClient::getSentBytes()`: returns the number of raw bytes sent to the broker (this includes meta information and not only message contents).
### Removals
_No functionality has been removed in this version._
-21
View File
@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) Marvin Mall <marvin-mall@msn.com>
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.
-424
View File
@@ -1,424 +0,0 @@
# php-mqtt/client
[![Latest Stable Version](https://poser.pugx.org/php-mqtt/client/v)](https://packagist.org/packages/php-mqtt/client)
[![Total Downloads](https://poser.pugx.org/php-mqtt/client/downloads)](https://packagist.org/packages/php-mqtt/client)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=coverage)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=alert_status)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=security_rating)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
[![License](https://poser.pugx.org/php-mqtt/client/license)](https://packagist.org/packages/php-mqtt/client)
[`php-mqtt/client`](https://packagist.org/packages/php-mqtt/client) was created by, and is maintained
by [Marvin Mall](https://github.com/namoshek).
It allows you to connect to an MQTT broker where you can publish messages and subscribe to topics.
The current implementation supports all QoS levels ([with limitations](#limitations)).
## Installation
The package is available on [packagist.org](https://packagist.org/packages/php-mqtt/client) and can be installed using `composer`:
```bash
composer require php-mqtt/client
```
The package requires PHP version 8.0 or higher.
## Usage
In the following, only a few very basic examples are given. For more elaborate examples, have a look at the
[`php-mqtt/client-examples` repository](https://github.com/php-mqtt/client-examples).
### Publish
A very basic publish example using QoS 0 requires only three steps: connect, publish and disconnect
```php
$server = 'some-broker.example.com';
$port = 1883;
$clientId = 'test-publisher';
$mqtt = new \PhpMqtt\Client\MqttClient($server, $port, $clientId);
$mqtt->connect();
$mqtt->publish('php-mqtt/client/test', 'Hello World!', 0);
$mqtt->disconnect();
```
If you do not want to pass a `$clientId`, a random one will be generated for you. This will basically force a clean session implicitly.
Be also aware that most of the methods can throw exceptions. The above example does not add any exception handling for brevity.
### Subscribe
Subscribing is a little more complex than publishing as it requires to run an event loop which reads, parses and handles messages from the broker:
```php
$server = 'some-broker.example.com';
$port = 1883;
$clientId = 'test-subscriber';
$mqtt = new \PhpMqtt\Client\MqttClient($server, $port, $clientId);
$mqtt->connect();
$mqtt->subscribe('php-mqtt/client/test', function ($topic, $message, $retained, $matchedWildcards) {
echo sprintf("Received message on topic [%s]: %s\n", $topic, $message);
}, 0);
$mqtt->loop(true);
$mqtt->disconnect();
```
While the loop is active, you can use `$mqtt->interrupt()` to send an interrupt signal to the loop.
This will terminate the loop before it starts its next iteration. You can call this method using `pcntl_signal(SIGINT, $handler)` for example:
```php
pcntl_async_signals(true);
$clientId = 'test-subscriber';
$mqtt = new \PhpMqtt\Client\MqttClient($server, $port, $clientId);
pcntl_signal(SIGINT, function (int $signal, $info) use ($mqtt) {
$mqtt->interrupt();
});
$mqtt->connect();
$mqtt->subscribe('php-mqtt/client/test', function ($topic, $message, $retained, $matchedWildcards) {
echo sprintf("Received message on topic [%s]: %s\n", $topic, $message);
}, 0);
$mqtt->loop(true);
$mqtt->disconnect();
```
### Client Settings
As shown in the examples above, the `MqttClient` takes the server, port and client id as first, second and third parameter.
As fourth parameter, the protocol level can be passed. Currently supported is MQTT v3.1,
available as constant `MqttClient::MQTT_3_1`.
A fifth parameter allows passing a repository (currently, only a `MemoryRepository` is available by default).
Lastly, a logger can be passed as sixth parameter. If none is given, a null logger is used instead.
Example:
```php
$mqtt = new \PhpMqtt\Client\MqttClient(
$server,
$port,
$clientId,
\PhpMqtt\Client\MqttClient::MQTT_3_1,
new \PhpMqtt\Client\Repositories\MemoryRepository(),
new Logger()
);
```
The `Logger` must implement the `Psr\Log\LoggerInterface`.
### Connection Settings
The `connect()` method of the `MqttClient` takes two optional parameters:
1. A `ConnectionSettings` instance
2. A `boolean` flag indicating whether a clean session should be requested (a random client id does this implicitly)
Example:
```php
$mqtt = new \PhpMqtt\Client\MqttClient($server, $port, $clientId);
$connectionSettings = (new \PhpMqtt\Client\ConnectionSettings)
->setConnectTimeout(3)
->setUseTls(true)
->setTlsSelfSignedAllowed(true);
$mqtt->connect($connectionSettings, true);
```
The `ConnectionSettings` class provides a few settings through a fluent interface. The type itself is immutable,
and a new `ConnectionSettings` instance will be created for each added option.
This also prevents changes to the connection settings after a connection has been established.
The following is a complete list of options with their respective default:
```php
$connectionSettings = (new \PhpMqtt\Client\ConnectionSettings)
// The username used for authentication when connecting to the broker.
->setUsername(null)
// The password used for authentication when connecting to the broker.
->setPassword(null)
// Whether to use a blocking socket when publishing messages or not.
// Normally, this setting can be ignored. When publishing large messages with multiple kilobytes in size,
// a blocking socket may be required if the receipt buffer of the broker is not large enough.
//
// Note: This setting has no effect on subscriptions, only on the publishing of messages.
->useBlockingSocket(false)
// The connect timeout defines the maximum amount of seconds the client will try to establish
// a socket connection with the broker. The value cannot be less than 1 second.
->setConnectTimeout(60)
// The socket timeout is the maximum amount of idle time in seconds for the socket connection.
// If no data is read or sent for the given amount of seconds, the socket will be closed.
// The value cannot be less than 1 second.
->setSocketTimeout(5)
// The resend timeout is the number of seconds the client will wait before sending a duplicate
// of pending messages without acknowledgement. The value cannot be less than 1 second.
->setResendTimeout(10)
// This flag determines whether the client will try to reconnect automatically
// if it notices a disconnect while sending data.
// The setting cannot be used together with the clean session flag.
->setReconnectAutomatically(false)
// Defines the maximum number of reconnect attempts until the client gives up.
// This setting is only relevant if setReconnectAutomatically() is set to true.
->setMaxReconnectAttempts(3)
// Defines the delay between reconnect attempts in milliseconds.
// This setting is only relevant if setReconnectAutomatically() is set to true.
->setDelayBetweenReconnectAttempts(0)
// The keep alive interval is the number of seconds the client will wait without sending a message
// until it sends a keep alive signal (ping) to the broker. The value cannot be less than 1 second
// and may not be higher than 65535 seconds. A reasonable value is 10 seconds (the default).
->setKeepAliveInterval(10)
// If the broker should publish a last will message in the name of the client when the client
// disconnects abruptly, this setting defines the topic on which the message will be published.
//
// A last will message will only be published if both this setting as well as the last will
// message are configured.
->setLastWillTopic(null)
// If the broker should publish a last will message in the name of the client when the client
// disconnects abruptly, this setting defines the message which will be published.
//
// A last will message will only be published if both this setting as well as the last will
// topic are configured.
->setLastWillMessage(null)
// The quality of service level the last will message of the client will be published with,
// if it gets triggered.
->setLastWillQualityOfService(0)
// This flag determines if the last will message of the client will be retained, if it gets
// triggered. Using this setting can be handy to signal that a client is offline by publishing
// a retained offline state in the last will and an online state as first message on connect.
->setRetainLastWill(false)
// This flag determines if TLS should be used for the connection. The port which is used to
// connect to the broker must support TLS connections.
->setUseTls(false)
// This flag determines if the peer certificate is verified, if TLS is used.
->setTlsVerifyPeer(true)
// This flag determines if the peer name is verified, if TLS is used.
->setTlsVerifyPeerName(true)
// This flag determines if self signed certificates of the peer should be accepted.
// Setting this to TRUE implies a security risk and should be avoided for production
// scenarios and public services.
->setTlsSelfSignedAllowed(false)
// The path to a Certificate Authority certificate which is used to verify the peer
// certificate, if TLS is used.
->setTlsCertificateAuthorityFile(null)
// The path to a directory containing Certificate Authority certificates which are
// used to verify the peer certificate, if TLS is used.
->setTlsCertificateAuthorityPath(null)
// The path to a client certificate file used for authentication, if TLS is used.
//
// The client certificate must be PEM encoded. It may optionally contain the
// certificate chain of issuers.
->setTlsClientCertificateFile(null)
// The path to a client certificate key file used for authentication, if TLS is used.
//
// This option requires ConnectionSettings::setTlsClientCertificateFile() to be used as well.
->setTlsClientCertificateKeyFile(null)
// The passphrase used to decrypt the private key of the client certificate,
// which in return is used for authentication, if TLS is used.
//
// This option requires ConnectionSettings::setTlsClientCertificateFile() and
// ConnectionSettings::setTlsClientCertificateKeyFile() to be used as well.
->setTlsClientCertificateKeyPassphrase(null);
// The TLS ALPN is used to establish a TLS encrypted mqtt connection on port 443,
// which usually is reserved for TLS encrypted HTTP traffic.
->setTlsAlpn(null);
```
### Hooks
The client includes a flexible and powerful hook system to allow custom behaviors during different stages of the MQTT lifecycle. Hooks are registered using closures and can be added or removed dynamically at runtime.
> 💡 All hooks receive the MQTT client instance (`MqttClient`) as their first argument, allowing full access to the client's capabilities from within the hook.
> 💡 Each hook is executed in a `try-catch` block to ensure no individual exception can crash the loop or hook processing.
#### Loop Event Hooks
Called on each iteration of the MQTT client's loop. This hook is especially useful to implement timeouts or other deadlock-prevention logic.
##### Register
```php
$callback = function (MqttClient $mqtt, float $elapsedTime) {
echo "Running for {$elapsedTime} seconds already.";
};
$mqtt->registerLoopEventHandler($callback);
```
##### Unregister
```php
$mqtt->unregisterLoopEventHandler($callback); // Unregister specific event handler
$mqtt->unregisterLoopEventHandler(); // Unregister all event handlers
```
#### Publish Event Hooks
Triggered every time a message is published to the broker. This hook is useful to implement centralized logging or metrics.
##### Register
```php
$callback = function (
MqttClient $mqtt,
string $topic,
string $message,
?int $messageId,
int $qualityOfService,
bool $retain
) {
echo "Published to [{$topic}]: {$message}";
};
$mqtt->registerPublishEventHandler($callback);
```
##### Unregister
```php
$mqtt->unregisterPublishEventHandler($callback); // Unregister specific event handler
$mqtt->unregisterPublishEventHandler(); // Unregister all event handlers
```
#### Message Received Hooks
Executed when a message is received from the broker as part of a subscription. This hook is useful to implement centralized logging or metrics.
##### Register
```php
$callback = function (
MqttClient $mqtt,
string $topic,
string $message,
int $qualityOfService,
bool $retained
) {
echo "Message on [{$topic}]: {$message}";
};
$mqtt->registerMessageReceivedEventHandler($callback);
```
##### Unregister
```php
$mqtt->unregisterMessageReceivedEventHandler($callback); // Unregister specific event handler
$mqtt->unregisterMessageReceivedEventHandler(); // Unregister all event handlers
```
#### Connected Hooks
Invoked when the client connects to the broker (initial or auto-reconnect).
##### Register
```php
$callback = function (MqttClient $mqtt, bool $isAutoReconnect) {
echo $isAutoReconnect ? "Auto-reconnected!" : "Connected!";
};
$mqtt->registerConnectedEventHandler($callback);
```
##### Unregister
```php
$mqtt->unregisterConnectedEventHandler($callback); // Unregister specific event handler
$mqtt->unregisterConnectedEventHandler(); // Unregister all event handlers
```
## Features
- Supported MQTT Versions
- [x] v3 (just don't use v3.1 features like username & password)
- [x] v3.1
- [x] v3.1.1
- [ ] v5.0
- Transport
- [x] TCP (unsecured)
- [x] TLS (secured, verifies the peer using a certificate authority file)
- Connect
- [x] Last Will
- [x] Message Retention
- [x] Authentication (username & password)
- [x] TLS encrypted connections
- [ ] Clean Session (can be set and sent, but the client has no persistence for QoS 2 messages)
- Publish
- [x] QoS Level 0
- [x] QoS Level 1 (limitation: no persisted state across sessions)
- [x] QoS Level 2 (limitation: no persisted state across sessions)
- Subscribe
- [x] QoS Level 0
- [x] QoS Level 1
- [x] QoS Level 2 (limitation: no persisted state across sessions)
- Supported Message Length: unlimited _(no limits enforced, although the MQTT protocol supports only up to 256MB which one shouldn't use even remotely anyway)_
- Logging possible (`Psr\Log\LoggerInterface` can be passed to the client)
- Persistence Drivers
- [x] In-Memory Driver
- [ ] Redis Driver
## Limitations
- Message flows with a QoS level higher than 0 are not persisted as the default implementation uses an in-memory repository for data.
To avoid issues with broken message flows, use the clean session flag to indicate that you don't care about old data.
It will not only instruct the broker to consider the connection new (without previous state), but will also reset the registered repository.
## Developing & Testing
### Certificates (TLS)
To run the tests (especially the TLS tests), you will need to create certificates. A command has been provided for this:
```sh
sh create-certificates.sh
```
This will create all required certificates in the `.ci/tls/` directory. The same script is used for continuous integration as well.
### MQTT Broker for Testing
Running the tests expects an MQTT broker to be running. The easiest way to run an MQTT broker is through Docker:
```sh
docker run --rm -it \
-p 1883:1883 \
-p 1884:1884 \
-p 8883:8883 \
-p 8884:8884 \
-v $(pwd)/.ci/tls:/mosquitto-certs \
-v $(pwd)/.ci/mosquitto.conf:/mosquitto/config/mosquitto.conf \
-v $(pwd)/.ci/mosquitto.passwd:/mosquitto/config/mosquitto.passwd \
eclipse-mosquitto:1.6
```
When run from the project directory, this will spawn a Mosquitto MQTT broker configured with the generated TLS certificates and a custom configuration.
In case you intend to run a different broker or using a different method, or use a public broker instead,
you will need to adjust the environment variables defined in `phpunit.xml` accordingly.
## License
`php-mqtt/client` is open-sourced software licensed under the [MIT license](LICENSE.md).
-53
View File
@@ -1,53 +0,0 @@
{
"name": "php-mqtt/client",
"description": "An MQTT client written in and for PHP.",
"type": "library",
"keywords": [
"mqtt",
"client",
"publish",
"subscribe"
],
"license": "MIT",
"authors": [
{
"name": "Marvin Mall",
"email": "marvin-mall@msn.com",
"role": "developer"
}
],
"autoload": {
"psr-4": {
"PhpMqtt\\Client\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"require": {
"php": "^8.0",
"psr/log": "^1.1|^2.0|^3.0",
"myclabs/php-enum": "^1.7"
},
"require-dev": {
"phpunit/php-invoker": "^3.0",
"phpunit/phpunit": "^9.0",
"squizlabs/php_codesniffer": "^3.5"
},
"suggest": {
"ext-redis": "Required for the RedisRepository"
},
"scripts": {
"fix:cs": "vendor/bin/phpcbf",
"test": [
"@test:cs",
"@test:all"
],
"test:all": "vendor/bin/phpunit --testdox --log-junit=phpunit.report-junit.xml --coverage-clover=phpunit.coverage-clover.xml --coverage-text",
"test:cs": "vendor/bin/phpcs",
"test:feature": "vendor/bin/phpunit --testsuite=Feature --testdox --log-junit=phpunit.report-junit.xml --coverage-clover=phpunit.coverage-clover.xml --coverage-text",
"test:unit": "vendor/bin/phpunit --testsuite=Unit --testdox --log-junit=phpunit.report-junit.xml --coverage-clover=phpunit.coverage-clover.xml --coverage-text"
}
}
-30
View File
@@ -1,30 +0,0 @@
#!/bin/sh
# Generate a new CA certificate and key.
openssl genrsa -out .ci/tls/ca.key 2048
openssl req -x509 -new -nodes -key .ci/tls/ca.key -days 1 -out .ci/tls/ca.crt -subj "/C=AT/ST=Vorarlberg/CN=php-mqtt Test CA"
# Copy ca.crt to a file named by the hashed subject of the certificate. This is required for PHP's capath option to find the certificate.
cp .ci/tls/ca.crt .ci/tls/$(openssl x509 -hash -noout -in .ci/tls/ca.crt).0
# Create a Java Trust Store from the CA certificate. This is used by HiveMQ.
keytool -import -file .ci/tls/ca.crt -alias ca -keystore .ci/tls/ca.jks -storepass s3cr3t -trustcacerts -noprompt
# Generate a new server certificate and key, signed by the created CA.
openssl genrsa -out .ci/tls/server.key 2048
openssl req -new -key .ci/tls/server.key -out .ci/tls/server.csr -sha512 -subj "/C=AT/ST=Vorarlberg/CN=localhost"
openssl x509 -req -in .ci/tls/server.csr -CA .ci/tls/ca.crt -CAkey .ci/tls/ca.key -CAcreateserial -out .ci/tls/server.crt -days 1 -sha512
# Generate a Java Key Store from the server certificate. This is used by HiveMQ.
openssl pkcs12 -export -in .ci/tls/server.crt -inkey .ci/tls/server.key -out .ci/tls/server.p12 -passout pass:s3cr3t
keytool -importkeystore -srckeystore .ci/tls/server.p12 -srcstoretype PKCS12 -destkeystore .ci/tls/server.jks -deststoretype JKS -srcstorepass s3cr3t -deststorepass s3cr3t -noprompt
# Generate a client certificate without passphrase, signed by the created CA.
openssl genrsa -out .ci/tls/client.key 2048
openssl req -new -key .ci/tls/client.key -out .ci/tls/client.csr -sha512 -subj "/C=AT/ST=Vorarlberg/CN=localhost"
openssl x509 -req -in .ci/tls/client.csr -CA .ci/tls/ca.crt -CAkey .ci/tls/ca.key -CAcreateserial -out .ci/tls/client.crt -days 1 -sha256
# Generate a client certificate with passphrase, signed by the created CA.
openssl genrsa -aes128 -passout pass:s3cr3t -out .ci/tls/client2.key 2048
openssl req -new -key .ci/tls/client2.key -passin pass:s3cr3t -out .ci/tls/client2.csr -sha512 -subj "/C=AT/ST=Vorarlberg/CN=localhost"
openssl x509 -req -in .ci/tls/client2.csr -CA .ci/tls/ca.crt -CAkey .ci/tls/ca.key -CAcreateserial -out .ci/tls/client2.crt -days 1 -sha256
-34
View File
@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
enforceTimeLimit="true"
defaultTimeLimit="3"
timeoutForSmallTests="2"
timeoutForMediumTests="5"
timeoutForLargeTests="10"
>
<php>
<env name="MQTT_BROKER_HOST" value="localhost"/>
<env name="MQTT_BROKER_PORT" value="1883"/>
<env name="MQTT_BROKER_PORT_WITH_AUTHENTICATION" value="1884"/>
<env name="MQTT_BROKER_TLS_PORT" value="8883"/>
<env name="MQTT_BROKER_TLS_WITH_CLIENT_CERT_PORT" value="8884"/>
<env name="TLS_CERT_DIR" value=".ci/tls"/>
<env name="SKIP_TLS_TESTS" value="false"/>
</php>
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">tests/Feature</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
</phpunit>
-18
View File
@@ -1,18 +0,0 @@
sonar.organization=php-mqtt
sonar.projectKey=php-mqtt_client
# Paths are relative to the sonar-project.properties file.
sonar.sources=src
sonar.tests=tests
# Test report and code coverage related settings.
sonar.php.tests.reportPath=phpunit.report-junit.xml
sonar.php.coverage.reportPaths=phpunit.coverage-clover.xml
# Encoding of the source code. Default is default system encoding.
sonar.sourceEncoding=UTF-8
# Links for sonarcloud.io page.
sonar.links.ci=https://github.com/php-mqtt/client/actions
sonar.links.scm=https://github.com/php-mqtt/client
sonar.links.issue=https://github.com/php-mqtt/client/issues
@@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Concerns;
/**
* Provides common methods used to generate random client ids.
*
* @package PhpMqtt\Client\Concerns
*/
trait GeneratesRandomClientIds
{
/**
* Generates a random client id in the form of an md5 hash.
*/
protected function generateRandomClientId(): string
{
return substr(md5(uniqid((string) random_int(0, PHP_INT_MAX), true)), 0, 20);
}
}
-301
View File
@@ -1,301 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Concerns;
use PhpMqtt\Client\Contracts\MqttClient;
/**
* Contains common methods and properties necessary to offer hooks.
*
* @mixin MqttClient
* @package PhpMqtt\Client\Concerns
*/
trait OffersHooks
{
/** @var \SplObjectStorage|array<\Closure> */
private $loopEventHandlers;
/** @var \SplObjectStorage|array<\Closure> */
private $publishEventHandlers;
/** @var \SplObjectStorage|array<\Closure> */
private $messageReceivedEventHandlers;
/** @var \SplObjectStorage|array<\Closure> */
private $connectedEventHandlers;
/**
* Needs to be called in order to initialize the trait.
*/
protected function initializeEventHandlers(): void
{
$this->loopEventHandlers = new \SplObjectStorage();
$this->publishEventHandlers = new \SplObjectStorage();
$this->messageReceivedEventHandlers = new \SplObjectStorage();
$this->connectedEventHandlers = new \SplObjectStorage();
}
/**
* Registers a loop event handler which is called each iteration of the loop.
* This event handler can be used for example to interrupt the loop under
* certain conditions.
*
* The loop event handler is passed the MQTT client instance as first and
* the elapsed time which the loop is already running for as second
* parameter. The elapsed time is a float containing seconds.
*
* Example:
* ```php
* $mqtt->registerLoopEventHandler(function (
* MqttClient $mqtt,
* float $elapsedTime
* ) use ($logger) {
* $logger->info("Running for [{$elapsedTime}] seconds already.");
* });
* ```
*
* Multiple event handlers can be registered at the same time.
*/
public function registerLoopEventHandler(\Closure $callback): MqttClient
{
$this->loopEventHandlers->attach($callback);
/** @var MqttClient $this */
return $this;
}
/**
* Unregisters a loop event handler which prevents it from being called
* in the future.
*
* This does not affect other registered event handlers. It is possible
* to unregister all registered event handlers by passing null as callback.
*/
public function unregisterLoopEventHandler(?\Closure $callback = null): MqttClient
{
if ($callback === null) {
$this->loopEventHandlers->removeAll($this->loopEventHandlers);
} else {
$this->loopEventHandlers->detach($callback);
}
/** @var MqttClient $this */
return $this;
}
/**
* Runs all registered loop event handlers with the given parameters.
* Each event handler is executed in a try-catch block to avoid spilling exceptions.
*/
private function runLoopEventHandlers(float $elapsedTime): void
{
foreach ($this->loopEventHandlers as $handler) {
try {
call_user_func($handler, $this, $elapsedTime);
} catch (\Throwable $e) {
$this->logger->error('Loop hook callback threw exception.', ['exception' => $e]);
}
}
}
/**
* Registers a loop event handler which is called when a message is published.
*
* The loop event handler is passed the MQTT client as first, the topic as
* second and the message as third parameter. As fourth parameter, the message identifier
* will be passed, which can be null in case of QoS 0. The QoS level as well as the retained
* flag will also be passed as fifth and sixth parameters.
*
* Example:
* ```php
* $mqtt->registerPublishEventHandler(function (
* MqttClient $mqtt,
* string $topic,
* string $message,
* ?int $messageId,
* int $qualityOfService,
* bool $retain
* ) use ($logger) {
* $logger->info("Sending message on topic [{$topic}]: {$message}");
* });
* ```
*
* Multiple event handlers can be registered at the same time.
*/
public function registerPublishEventHandler(\Closure $callback): MqttClient
{
$this->publishEventHandlers->attach($callback);
/** @var MqttClient $this */
return $this;
}
/**
* Unregisters a publish event handler which prevents it from being called
* in the future.
*
* This does not affect other registered event handlers. It is possible
* to unregister all registered event handlers by passing null as callback.
*/
public function unregisterPublishEventHandler(?\Closure $callback = null): MqttClient
{
if ($callback === null) {
$this->publishEventHandlers->removeAll($this->publishEventHandlers);
} else {
$this->publishEventHandlers->detach($callback);
}
/** @var MqttClient $this */
return $this;
}
/**
* Runs all the registered publish event handlers with the given parameters.
* Each event handler is executed in a try-catch block to avoid spilling exceptions.
*/
private function runPublishEventHandlers(string $topic, string $message, ?int $messageId, int $qualityOfService, bool $retain): void
{
foreach ($this->publishEventHandlers as $handler) {
try {
call_user_func($handler, $this, $topic, $message, $messageId, $qualityOfService, $retain);
} catch (\Throwable $e) {
$this->logger->error('Publish hook callback threw exception for published message on topic [{topic}].', [
'topic' => $topic,
'exception' => $e,
]);
}
}
}
/**
* Registers an event handler which is called when a message is received from the broker.
*
* The message received event handler is passed the MQTT client as first, the topic as
* second and the message as third parameter. As fourth parameter, the QoS level will be
* passed and the retained flag as fifth.
*
* Example:
* ```php
* $mqtt->registerReceivedMessageEventHandler(function (
* MqttClient $mqtt,
* string $topic,
* string $message,
* int $qualityOfService,
* bool $retained
* ) use ($logger) {
* $logger->info("Received message on topic [{$topic}]: {$message}");
* });
* ```
*
* Multiple event handlers can be registered at the same time.
*/
public function registerMessageReceivedEventHandler(\Closure $callback): MqttClient
{
$this->messageReceivedEventHandlers->attach($callback);
/** @var MqttClient $this */
return $this;
}
/**
* Unregisters a message received event handler which prevents it from being called in the future.
*
* This does not affect other registered event handlers. It is possible
* to unregister all registered event handlers by passing null as callback.
*/
public function unregisterMessageReceivedEventHandler(?\Closure $callback = null): MqttClient
{
if ($callback === null) {
$this->messageReceivedEventHandlers->removeAll($this->messageReceivedEventHandlers);
} else {
$this->messageReceivedEventHandlers->detach($callback);
}
/** @var MqttClient $this */
return $this;
}
/**
* Runs all the registered message received event handlers with the given parameters.
* Each event handler is executed in a try-catch block to avoid spilling exceptions.
*/
private function runMessageReceivedEventHandlers(string $topic, string $message, int $qualityOfService, bool $retained): void
{
foreach ($this->messageReceivedEventHandlers as $handler) {
try {
call_user_func($handler, $this, $topic, $message, $qualityOfService, $retained);
} catch (\Throwable $e) {
$this->logger->error('Received message hook callback threw exception for received message on topic [{topic}].', [
'topic' => $topic,
'exception' => $e,
]);
}
}
}
/**
* Registers an event handler which is called when the client established a connection to the broker.
* This also includes manual reconnects as well as auto-reconnects by the client itself.
*
* The event handler is passed the MQTT client as first argument,
* followed by a flag which indicates whether an auto-reconnect occurred as second argument.
*
* Example:
* ```php
* $mqtt->registerConnectedEventHandler(function (
* MqttClient $mqtt,
* bool $isAutoReconnect
* ) use ($logger) {
* if ($isAutoReconnect) {
* $logger->info("Client successfully auto-reconnected to the broker.);
* } else {
* $logger->info("Client successfully connected to the broker.");
* }
* });
* ```
*
* Multiple event handlers can be registered at the same time.
*/
public function registerConnectedEventHandler(\Closure $callback): MqttClient
{
$this->connectedEventHandlers->attach($callback);
/** @var MqttClient $this */
return $this;
}
/**
* Unregisters a connected event handler which prevents it from being called in the future.
*
* This does not affect other registered event handlers. It is possible
* to unregister all registered event handlers by passing null as callback.
*/
public function unregisterConnectedEventHandler(?\Closure $callback = null): MqttClient
{
if ($callback === null) {
$this->connectedEventHandlers->removeAll($this->connectedEventHandlers);
} else {
$this->connectedEventHandlers->detach($callback);
}
/** @var MqttClient $this */
return $this;
}
/**
* Runs all the registered connected event handlers.
* Each event handler is executed in a try-catch block to avoid spilling exceptions.
*/
private function runConnectedEventHandlers(bool $isAutoReconnect): void
{
foreach ($this->connectedEventHandlers as $handler) {
try {
call_user_func($handler, $this, $isAutoReconnect);
} catch (\Throwable $e) {
$this->logger->error('Connected hook callback threw exception.', ['exception' => $e]);
}
}
}
}
-78
View File
@@ -1,78 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Concerns;
/**
* Provides common methods to encode data before sending it to a broker
* and to decode data received from a broker.
*
* @package PhpMqtt\Client\Concerns
*/
trait TranscodesData
{
/**
* Creates a string which is prefixed with its own length as bytes.
* This means a string like 'hello world' will become
*
* \x00\x0bhello world
*
* where \x00\0x0b is the hex representation of 00000000 00001011 = 11
*/
protected function buildLengthPrefixedString(string $data): string
{
$length = strlen($data);
$msb = $length >> 8;
$lsb = $length % 256;
return chr($msb) . chr($lsb) . $data;
}
/**
* Converts the given string to a number, assuming it is an MSB encoded message id.
* MSB means preceding characters have higher value.
*/
protected function decodeMessageId(string $encodedMessageId): int
{
$length = strlen($encodedMessageId);
$result = 0;
foreach (str_split($encodedMessageId) as $index => $char) {
$result += ord($char) << (($length - 1) * 8 - ($index * 8));
}
return $result;
}
/**
* Encodes the given message identifier as string.
*/
protected function encodeMessageId(int $messageId): string
{
return chr($messageId >> 8) . chr($messageId % 256);
}
/**
* Encodes the length of a message as string, so it can be transmitted
* over the wire.
*/
protected function encodeMessageLength(int $length): string
{
$result = '';
do {
$digit = $length % 128;
$length = $length >> 7;
// if there are more digits to encode, set the top bit of this digit
if ($length > 0) {
$digit = ($digit | 0x80);
}
$result .= chr($digit);
} while ($length > 0);
return $result;
}
}
@@ -1,89 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Concerns;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\Exceptions\ConfigurationInvalidException;
use PhpMqtt\Client\MqttClient;
/**
* Provides methods to validate the configuration of an {@see MqttClient} and
* the {@see ConnectionSettings} being used to connect to a broker.
*
* @package PhpMqtt\Client\Concerns
*/
trait ValidatesConfiguration
{
/**
* Ensures the given connection settings are valid. If they are not valid,
* which means they are misconfigured, an exception containing information about
* the configuration error is thrown.
*
* @throws ConfigurationInvalidException
*/
protected function ensureConnectionSettingsAreValid(ConnectionSettings $settings): void
{
if ($settings->getConnectTimeout() < 1) {
throw new ConfigurationInvalidException('The connect timeout cannot be less than 1 second.');
}
if ($settings->getSocketTimeout() < 1) {
throw new ConfigurationInvalidException('The socket timeout cannot be less than 1 second.');
}
if ($settings->getResendTimeout() < 1) {
throw new ConfigurationInvalidException('The resend timeout cannot be less than 1 second.');
}
if ($settings->getKeepAliveInterval() < 1 || $settings->getKeepAliveInterval() > 65535) {
throw new ConfigurationInvalidException('The keep alive interval must be a value in the range of 1 to 65535 seconds.');
}
if ($settings->getMaxReconnectAttempts() < 1) {
throw new ConfigurationInvalidException('The maximum reconnect attempts cannot be fewer than 1.');
}
if ($settings->getDelayBetweenReconnectAttempts() < 0) {
throw new ConfigurationInvalidException('The delay between reconnect attempts cannot be lower than 0.');
}
if ($settings->getUsername() !== null && trim($settings->getUsername()) === '') {
throw new ConfigurationInvalidException('The username may not consist of white space only.');
}
if ($settings->getLastWillTopic() !== null && trim($settings->getLastWillTopic()) === '') {
throw new ConfigurationInvalidException('The last will topic may not consist of white space only.');
}
if ($settings->getLastWillQualityOfService() < MqttClient::QOS_AT_MOST_ONCE
|| $settings->getLastWillQualityOfService() > MqttClient::QOS_EXACTLY_ONCE) {
throw new ConfigurationInvalidException('The QoS for the last will must be a value in the range of 0 to 2.');
}
if ($settings->getTlsCertificateAuthorityFile() !== null && !is_file($settings->getTlsCertificateAuthorityFile())) {
throw new ConfigurationInvalidException('The Certificate Authority file setting must contain the path to a regular file.');
}
if ($settings->getTlsCertificateAuthorityPath() !== null && !is_dir($settings->getTlsCertificateAuthorityPath())) {
throw new ConfigurationInvalidException('The Certificate Authority path setting must contain the path to a directory.');
}
if ($settings->getTlsClientCertificateFile() !== null && !is_file($settings->getTlsClientCertificateFile())) {
throw new ConfigurationInvalidException('The client certificate file setting must contain the path to a regular file.');
}
if ($settings->getTlsClientCertificateKeyFile() !== null && !is_file($settings->getTlsClientCertificateKeyFile())) {
throw new ConfigurationInvalidException('The client certificate key file setting must contain the path to a regular file.');
}
if ($settings->getTlsClientCertificateKeyFile() !== null && $settings->getTlsClientCertificateFile() === null) {
throw new ConfigurationInvalidException('Using a client certificate key file without certificate does not work.');
}
if ($settings->getTlsClientCertificateKeyPassphrase() !== null && $settings->getTlsClientCertificateKeyFile() === null) {
throw new ConfigurationInvalidException('Using a client certificate key passphrase without key file does not work.');
}
}
}
@@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Concerns;
/**
* Provides common methods to work with buffers.
*
* @package PhpMqtt\Client\Concerns
*/
trait WorksWithBuffers
{
/**
* Pops the first $limit bytes from the given buffer and returns them.
*/
protected function pop(string &$buffer, int $limit): string
{
$limit = min(strlen($buffer), $limit);
$result = substr($buffer, 0, $limit);
$buffer = substr($buffer, $limit);
return $result;
}
}
-555
View File
@@ -1,555 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client;
/**
* The settings used during connection to a broker.
*
* This class is immutable and all setters return a clone of the original class because
* connection settings must not change once passed to MqttClient.
*
* @package PhpMqtt\Client
*/
class ConnectionSettings
{
private ?string $username = null;
private ?string $password = null;
private bool $useBlockingSocket = false;
private int $connectTimeout = 60;
private int $socketTimeout = 5;
private int $resendTimeout = 10;
private int $keepAliveInterval = 10;
private bool $reconnectAutomatically = false;
private int $maxReconnectAttempts = 3;
private int $delayBetweenReconnectAttempts = 0;
private ?string $lastWillTopic = null;
private ?string $lastWillMessage = null;
private int $lastWillQualityOfService = 0;
private bool $lastWillRetain = false;
private bool $useTls = false;
private bool $tlsVerifyPeer = true;
private bool $tlsVerifyPeerName = true;
private bool $tlsSelfSignedAllowed = false;
private ?string $tlsCertificateAuthorityFile = null;
private ?string $tlsCertificateAuthorityPath = null;
private ?string $tlsClientCertificateFile = null;
private ?string $tlsClientCertificateKeyFile = null;
private ?string $tlsClientCertificateKeyPassphrase = null;
private ?string $tlsAlpn = null;
/**
* The username used for authentication when connecting to the broker.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setUsername(?string $username): ConnectionSettings
{
$copy = clone $this;
$copy->username = $username;
return $copy;
}
public function getUsername(): ?string
{
return $this->username;
}
/**
* The password used for authentication when connecting to the broker.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setPassword(?string $password): ConnectionSettings
{
$copy = clone $this;
$copy->password = $password;
return $copy;
}
public function getPassword(): ?string
{
return $this->password;
}
/**
* Whether to use a blocking socket when publishing messages or not.
* Normally, this setting can be ignored. When publishing large messages with multiple kilobytes in size,
* a blocking socket may be required if the receipt buffer of the broker is not large enough.
*
* Note: This setting has no effect on subscriptions, only on the publishing of messages.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function useBlockingSocket(bool $useBlockingSocket): ConnectionSettings
{
$copy = clone $this;
$copy->useBlockingSocket = $useBlockingSocket;
return $copy;
}
public function shouldUseBlockingSocket(): bool
{
return $this->useBlockingSocket;
}
/**
* The connect timeout is the maximum amount of seconds the client will try to establish
* a socket connection with the broker. The value cannot be less than 1 second.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setConnectTimeout(int $connectTimeout): ConnectionSettings
{
$copy = clone $this;
$copy->connectTimeout = $connectTimeout;
return $copy;
}
public function getConnectTimeout(): int
{
return $this->connectTimeout;
}
/**
* The socket timeout is the maximum amount of idle time in seconds for the socket connection.
* If no data is read or sent for the given amount of seconds, the socket will be closed.
* The value cannot be less than 1 second.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setSocketTimeout(int $socketTimeout): ConnectionSettings
{
$copy = clone $this;
$copy->socketTimeout = $socketTimeout;
return $copy;
}
public function getSocketTimeout(): int
{
return $this->socketTimeout;
}
/**
* The resend timeout is the number of seconds the client will wait before sending a duplicate
* of pending messages without acknowledgement. The value cannot be less than 1 second.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setResendTimeout(int $resendTimeout): ConnectionSettings
{
$copy = clone $this;
$copy->resendTimeout = $resendTimeout;
return $copy;
}
public function getResendTimeout(): int
{
return $this->resendTimeout;
}
/**
* The keep alive interval is the number of seconds the client will wait without sending a message
* until it sends a keep alive signal (ping) to the broker. The value cannot be less than 1 second
* and may not be higher than 65535 seconds. A reasonable value is 10 seconds (the default).
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setKeepAliveInterval(int $keepAliveInterval): ConnectionSettings
{
$copy = clone $this;
$copy->keepAliveInterval = $keepAliveInterval;
return $copy;
}
public function getKeepAliveInterval(): int
{
return $this->keepAliveInterval;
}
/**
* This flag determines whether the client will try to reconnect automatically,
* if it notices a disconnect while sending data.
* The setting cannot be used together with the clean session flag.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setReconnectAutomatically(bool $reconnectAutomatically): ConnectionSettings
{
$copy = clone $this;
$copy->reconnectAutomatically = $reconnectAutomatically;
return $copy;
}
public function shouldReconnectAutomatically(): bool
{
return $this->reconnectAutomatically;
}
/**
* Defines the maximum number of reconnect attempts until the client gives up. This setting
* is only relevant if {@see setReconnectAutomatically()} is set to true.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setMaxReconnectAttempts(int $maxReconnectAttempts): ConnectionSettings
{
$copy = clone $this;
$copy->maxReconnectAttempts = $maxReconnectAttempts;
return $copy;
}
public function getMaxReconnectAttempts(): int
{
return $this->maxReconnectAttempts;
}
/**
* Defines the delay between reconnect attempts in milliseconds.
* This setting is only relevant if {@see setReconnectAutomatically()} is set to true.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setDelayBetweenReconnectAttempts(int $delayBetweenReconnectAttempts): ConnectionSettings
{
$copy = clone $this;
$copy->delayBetweenReconnectAttempts = $delayBetweenReconnectAttempts;
return $copy;
}
public function getDelayBetweenReconnectAttempts(): int
{
return $this->delayBetweenReconnectAttempts;
}
/**
* If the broker should publish a last will message in the name of the client when the client
* disconnects abruptly, this setting defines the topic on which the message will be published.
*
* A last will message will only be published if both this setting as well as the last will
* message are configured.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setLastWillTopic(?string $lastWillTopic): ConnectionSettings
{
$copy = clone $this;
$copy->lastWillTopic = $lastWillTopic;
return $copy;
}
public function getLastWillTopic(): ?string
{
return $this->lastWillTopic;
}
/**
* If the broker should publish a last will message in the name of the client when the client
* disconnects abruptly, this setting defines the message which will be published.
*
* A last will message will only be published if both this setting as well as the last will
* topic are configured.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setLastWillMessage(?string $lastWillMessage): ConnectionSettings
{
$copy = clone $this;
$copy->lastWillMessage = $lastWillMessage;
return $copy;
}
public function getLastWillMessage(): ?string
{
return $this->lastWillMessage;
}
/**
* Determines whether the client has a last will.
*/
public function hasLastWill(): bool
{
return $this->lastWillTopic !== null && $this->lastWillMessage !== null;
}
/**
* The quality of service level the last will message of the client will be published with,
* if it gets triggered.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setLastWillQualityOfService(int $lastWillQualityOfService): ConnectionSettings
{
$copy = clone $this;
$copy->lastWillQualityOfService = $lastWillQualityOfService;
return $copy;
}
public function getLastWillQualityOfService(): int
{
return $this->lastWillQualityOfService;
}
/**
* This flag determines if the last will message of the client will be retained, if it gets
* triggered. Using this setting can be handy to signal that a client is offline by publishing
* a retained offline state in the last will and an online state as first message on connect.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setRetainLastWill(bool $lastWillRetain): ConnectionSettings
{
$copy = clone $this;
$copy->lastWillRetain = $lastWillRetain;
return $copy;
}
public function shouldRetainLastWill(): bool
{
return $this->lastWillRetain;
}
/**
* This flag determines if TLS should be used for the connection. The port which is used to
* connect to the broker must support TLS connections.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setUseTls(bool $useTls): ConnectionSettings
{
$copy = clone $this;
$copy->useTls = $useTls;
return $copy;
}
public function shouldUseTls(): bool
{
return $this->useTls;
}
/**
* This flag determines if the peer certificate is verified, if TLS is used.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsVerifyPeer(bool $tlsVerifyPeer): ConnectionSettings
{
$copy = clone $this;
$copy->tlsVerifyPeer = $tlsVerifyPeer;
return $copy;
}
public function shouldTlsVerifyPeer(): bool
{
return $this->tlsVerifyPeer;
}
/**
* This flag determines if the peer name is verified, if TLS is used.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsVerifyPeerName(bool $tlsVerifyPeerName): ConnectionSettings
{
$copy = clone $this;
$copy->tlsVerifyPeerName = $tlsVerifyPeerName;
return $copy;
}
public function shouldTlsVerifyPeerName(): bool
{
return $this->tlsVerifyPeerName;
}
/**
* This flag determines if self signed certificates of the peer should be accepted.
* Setting this to TRUE implies a security risk and should be avoided for production
* scenarios and public services.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsSelfSignedAllowed(bool $tlsSelfSignedAllowed): ConnectionSettings
{
$copy = clone $this;
$copy->tlsSelfSignedAllowed = $tlsSelfSignedAllowed;
return $copy;
}
public function isTlsSelfSignedAllowed(): bool
{
return $this->tlsSelfSignedAllowed;
}
/**
* The path to a Certificate Authority certificate which is used to verify the peer
* certificate, if TLS is used.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsCertificateAuthorityFile(?string $tlsCertificateAuthorityFile): ConnectionSettings
{
$copy = clone $this;
$copy->tlsCertificateAuthorityFile = $tlsCertificateAuthorityFile;
return $copy;
}
public function getTlsCertificateAuthorityFile(): ?string
{
return $this->tlsCertificateAuthorityFile;
}
/**
* The path to a directory containing Certificate Authority certificates which are
* used to verify the peer certificate, if TLS is used.
*
* Certificate files in this directory must be named by the hash of the certificate,
* ending with ".0" (without quotes). The certificate hash can be retrieved using the
* openssl_x509_parse() function, which returns an array. The hash can be found in the
* array under the key "hash".
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsCertificateAuthorityPath(?string $tlsCertificateAuthorityPath): ConnectionSettings
{
$copy = clone $this;
$copy->tlsCertificateAuthorityPath = $tlsCertificateAuthorityPath;
return $copy;
}
public function getTlsCertificateAuthorityPath(): ?string
{
return $this->tlsCertificateAuthorityPath;
}
/**
* The path to a client certificate file used for authentication, if TLS is used.
*
* The client certificate must be PEM encoded. It may optionally contain the
* certificate chain of issuers. The certificate key can be included in this certificate
* file or in a separate file ({@see ConnectionSettings::setTlsClientCertificateKeyFile()}).
* A passphrase can be configured using {@see ConnectionSettings::setTlsClientCertificateKeyPassphrase()}.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsClientCertificateFile(?string $tlsClientCertificateFile): ConnectionSettings
{
$copy = clone $this;
$copy->tlsClientCertificateFile = $tlsClientCertificateFile;
return $copy;
}
public function getTlsClientCertificateFile(): ?string
{
return $this->tlsClientCertificateFile;
}
/**
* The path to a client certificate key file used for authentication, if TLS is used.
*
* This option requires {@see ConnectionSettings::setTlsClientCertificateFile()}
* to be used as well.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsClientCertificateKeyFile(?string $tlsClientCertificateKeyFile): ConnectionSettings
{
$copy = clone $this;
$copy->tlsClientCertificateKeyFile = $tlsClientCertificateKeyFile;
return $copy;
}
public function getTlsClientCertificateKeyFile(): ?string
{
return $this->tlsClientCertificateKeyFile;
}
/**
* The passphrase used to decrypt the private key of the client certificate,
* which in return is used for authentication, if TLS is used.
*
* This option requires {@see ConnectionSettings::setTlsClientCertificateFile()}
* and {@see ConnectionSettings::setTlsClientCertificateKeyFile()} to be used as well.
*
* Please be aware that your passphrase is not stored in secure memory when using this option.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsClientCertificateKeyPassphrase(?string $tlsClientCertificateKeyPassphrase): ConnectionSettings
{
$copy = clone $this;
$copy->tlsClientCertificateKeyPassphrase = $tlsClientCertificateKeyPassphrase;
return $copy;
}
public function getTlsClientCertificateKeyPassphrase(): ?string
{
return $this->tlsClientCertificateKeyPassphrase;
}
/**
* The TLS ALPN is used to establish a TLS encrypted mqtt connection on port 443,
* which usually is reserved for TLS encrypted HTTP traffic.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsAlpn(?string $tlsAlpn): ConnectionSettings
{
$copy = clone $this;
$copy->tlsAlpn = $tlsAlpn;
return $copy;
}
public function getTlsAlpn(): ?string
{
return $this->tlsAlpn;
}
}
@@ -1,118 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Contracts;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\Exceptions\ConnectingToBrokerFailedException;
use PhpMqtt\Client\Exceptions\InvalidMessageException;
use PhpMqtt\Client\Exceptions\MqttClientException;
use PhpMqtt\Client\Exceptions\ProtocolViolationException;
use PhpMqtt\Client\Message;
use PhpMqtt\Client\Subscription;
/**
* Implementations of this interface provide message parsing capabilities.
* Services of this type are used by the {@see MqttClient} to implement multiple protocol versions.
*
* @package PhpMqtt\Client\Contracts
*/
interface MessageProcessor
{
/**
* Try to parse a message from the incoming buffer. If a message could be parsed successfully,
* the given message parameter is set to the parsed message and the result is true.
* If no message could be parsed, the result is false and the required bytes parameter indicates
* how many bytes are missing for the message to be complete. If this parameter is set to -1,
* it means we have no (or not yet) knowledge about the required bytes.
*/
public function tryFindMessageInBuffer(string $buffer, int $bufferLength, ?string &$message = null, int &$requiredBytes = -1): bool;
/**
* Parses and validates the given message based on its message type and contents.
* If no valid message could be found in the data, and no further action is required by the caller,
* null is returned.
*
* @throws InvalidMessageException
* @throws ProtocolViolationException
* @throws MqttClientException
*/
public function parseAndValidateMessage(string $message): ?Message;
/**
* Builds a connect message from the given connection settings, taking the protocol
* specifics into account.
*/
public function buildConnectMessage(ConnectionSettings $connectionSettings, bool $useCleanSession = false): string;
/**
* Builds a ping request message.
*/
public function buildPingRequestMessage(): string;
/**
* Builds a ping response message.
*/
public function buildPingResponseMessage(): string;
/**
* Builds a disconnect message.
*/
public function buildDisconnectMessage(): string;
/**
* Builds a subscribe message from the given parameters.
*
* @param Subscription[] $subscriptions
*/
public function buildSubscribeMessage(int $messageId, array $subscriptions, bool $isDuplicate = false): string;
/**
* Builds an unsubscribe message from the given parameters.
*
* @param string[] $topics
*/
public function buildUnsubscribeMessage(int $messageId, array $topics, bool $isDuplicate = false): string;
/**
* Builds a publish message based on the given parameters.
*/
public function buildPublishMessage(
string $topic,
string $message,
int $qualityOfService,
bool $retain,
?int $messageId = null,
bool $isDuplicate = false,
): string;
/**
* Builds a publish acknowledgement for the given message identifier.
*/
public function buildPublishAcknowledgementMessage(int $messageId): string;
/**
* Builds a publish received message for the given message identifier.
*/
public function buildPublishReceivedMessage(int $messageId): string;
/**
* Builds a publish release message for the given message identifier.
*/
public function buildPublishReleaseMessage(int $messageId): string;
/**
* Builds a publish complete message for the given message identifier.
*/
public function buildPublishCompleteMessage(int $messageId): string;
/**
* Handles the connect acknowledgement received from the broker. Exits normally if the
* connection could be established successfully according to the response. Throws an
* exception if the broker responded with an error.
*
* @throws ConnectingToBrokerFailedException
*/
public function handleConnectAcknowledgement(string $message): void;
}
-266
View File
@@ -1,266 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Contracts;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\Exceptions\ConfigurationInvalidException;
use PhpMqtt\Client\Exceptions\ConnectingToBrokerFailedException;
use PhpMqtt\Client\Exceptions\DataTransferException;
use PhpMqtt\Client\Exceptions\InvalidMessageException;
use PhpMqtt\Client\Exceptions\MqttClientException;
use PhpMqtt\Client\Exceptions\ProtocolViolationException;
use PhpMqtt\Client\Exceptions\RepositoryException;
/**
* An interface for the MQTT client.
*
* @package PhpMqtt\Client\Contracts
*/
interface MqttClient
{
/**
* Connect to the MQTT broker using the given settings.
* If no custom settings are passed, the client will use the default settings.
* See {@see ConnectionSettings} for more details about the defaults.
*
* @throws ConfigurationInvalidException
* @throws ConnectingToBrokerFailedException
*/
public function connect(?ConnectionSettings $settings = null, bool $useCleanSession = false): void;
/**
* Sends a disconnect message to the broker and closes the socket.
*
* @throws DataTransferException
*/
public function disconnect(): void;
/**
* Returns an indication, whether the client is supposed to be connected already or not.
*
* Note: the result of this method should be used carefully, since we can only detect a
* closed socket once we try to send or receive data. Therefore, this method only gives
* an indication whether the client is in a connected state or not.
*
* This information may be useful in applications where multiple parts use the client.
*/
public function isConnected(): bool;
/**
* Publishes the given message on the given topic. If the additional quality of service
* and retention flags are set, the message will be published using these settings.
*
* @throws DataTransferException
* @throws RepositoryException
*/
public function publish(string $topic, string $message, int $qualityOfService = 0, bool $retain = false): void;
/**
* Subscribe to the given topic with the given quality of service.
*
* The subscription callback is passed the topic as first and the message as second
* parameter. A third parameter indicates whether the received message has been sent
* because it was retained by the broker. A fourth parameter contains matched topic wildcards.
*
* Example:
* ```php
* $mqtt->subscribe(
* '/foo/bar/+',
* function (string $topic, string $message, bool $retained, array $matchedWildcards) use ($logger) {
* $logger->info("Received {retained} message on topic [{topic}]: {message}", [
* 'topic' => $topic,
* 'message' => $message,
* 'retained' => $retained ? 'retained' : 'live'
* ]);
* }
* );
* ```
*
* If no callback is passed, a subscription will still be made. Received messages are delivered only to
* event handlers for received messages though.
*
* @throws DataTransferException
* @throws RepositoryException
*/
public function subscribe(string $topicFilter, ?callable $callback = null, int $qualityOfService = 0): void;
/**
* Unsubscribe from the given topic.
*
* @throws DataTransferException
* @throws RepositoryException
*/
public function unsubscribe(string $topicFilter): void;
/**
* Sets the interrupted signal. Doing so instructs the client to exit the loop, if it is
* actually looping.
*
* Sending multiple interrupt signals has no effect, unless the client exits the loop,
* which resets the signal for another loop.
*/
public function interrupt(): void;
/**
* Runs an event loop that handles messages from the server and calls the registered
* callbacks for published messages.
*
* If the second parameter is provided, the loop will exit as soon as all
* queues are empty. This means there may be no open subscriptions,
* no pending messages as well as acknowledgments and no pending unsubscribe requests.
*
* The third parameter will, if set, lead to a forceful exit after the specified
* amount of seconds, but only if the second parameter is set to true. This basically
* means that if we wait for all pending messages to be acknowledged, we only wait
* a maximum of $queueWaitLimit seconds until we give up. We do not exit after the
* given amount of time if there are open topic subscriptions though.
*
* @throws DataTransferException
* @throws InvalidMessageException
* @throws MqttClientException
* @throws ProtocolViolationException
*/
public function loop(bool $allowSleep = true, bool $exitWhenQueuesEmpty = false, ?int $queueWaitLimit = null): void;
/**
* Runs an event loop iteration that handles messages from the server and calls the registered
* callbacks for published messages. Also resends pending messages and calls loop event handlers.
*
* This method can be used to integrate the MQTT client in another event loop (like ReactPHP or Ratchet).
*
* Note: To ensure the event handlers called by this method will receive the correct elapsed time,
* the caller is responsible to provide the correct starting time of the loop as returned by `microtime(true)`.
*
* @throws DataTransferException
* @throws InvalidMessageException
* @throws MqttClientException
* @throws ProtocolViolationException
*/
public function loopOnce(float $loopStartedAt, bool $allowSleep = false, int $sleepMicroseconds = 100000): void;
/**
* Returns the host used by the client to connect to.
*/
public function getHost(): string;
/**
* Returns the port used by the client to connect to.
*/
public function getPort(): int;
/**
* Returns the identifier used by the client.
*/
public function getClientId(): string;
/**
* Returns the total number of received bytes, across reconnects.
*/
public function getReceivedBytes(): int;
/**
* Returns the total number of sent bytes, across reconnects.
*/
public function getSentBytes(): int;
/**
* Registers a loop event handler which is called each iteration of the loop.
* This event handler can be used for example to interrupt the loop under
* certain conditions.
*
* The loop event handler is passed the MQTT client instance as first and
* the elapsed time which the loop is already running for as second
* parameter. The elapsed time is a float containing seconds.
*
* Example:
* ```php
* $mqtt->registerLoopEventHandler(function (
* MqttClient $mqtt,
* float $elapsedTime
* ) use ($logger) {
* $logger->info("Running for [{$elapsedTime}] seconds already.");
* });
* ```
*
* Multiple event handlers can be registered at the same time.
*/
public function registerLoopEventHandler(\Closure $callback): MqttClient;
/**
* Unregisters a loop event handler which prevents it from being called
* in the future.
*
* This does not affect other registered event handlers. It is possible
* to unregister all registered event handlers by passing null as callback.
*/
public function unregisterLoopEventHandler(?\Closure $callback = null): MqttClient;
/**
* Registers a loop event handler which is called when a message is published.
*
* The loop event handler is passed the MQTT client as first, the topic as
* second and the message as third parameter. As fourth parameter, the
* message identifier will be passed. The QoS level as well as the retained
* flag will also be passed as fifth and sixth parameters.
*
* Example:
* ```php
* $mqtt->registerPublishEventHandler(function (
* MqttClient $mqtt,
* string $topic,
* string $message,
* int $messageId,
* int $qualityOfService,
* bool $retain
* ) use ($logger) {
* $logger->info("Received message on topic [{$topic}]: {$message}");
* });
* ```
*
* Multiple event handlers can be registered at the same time.
*/
public function registerPublishEventHandler(\Closure $callback): MqttClient;
/**
* Unregisters a publish event handler which prevents it from being called
* in the future.
*
* This does not affect other registered event handlers. It is possible
* to unregister all registered event handlers by passing null as callback.
*/
public function unregisterPublishEventHandler(?\Closure $callback = null): MqttClient;
/**
* Registers an event handler which is called when a message is received from the broker.
*
* The message received event handler is passed the MQTT client as first, the topic as
* second and the message as third parameter. As fourth parameter, the QoS level will be
* passed and the retained flag as fifth.
*
* Example:
* ```php
* $mqtt->registerReceivedMessageEventHandler(function (
* MqttClient $mqtt,
* string $topic,
* string $message,
* int $qualityOfService,
* bool $retained
* ) use ($logger) {
* $logger->info("Received message on topic [{$topic}]: {$message}");
* });
* ```
*
* Multiple event handlers can be registered at the same time.
*/
public function registerMessageReceivedEventHandler(\Closure $callback): MqttClient;
/**
* Unregisters a message received event handler which prevents it from being called in the future.
*
* This does not affect other registered event handlers. It is possible
* to unregister all registered event handlers by passing null as callback.
*/
public function unregisterMessageReceivedEventHandler(?\Closure $callback = null): MqttClient;
}
-143
View File
@@ -1,143 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Contracts;
use DateTime;
use PhpMqtt\Client\Exceptions\PendingMessageAlreadyExistsException;
use PhpMqtt\Client\Exceptions\PendingMessageNotFoundException;
use PhpMqtt\Client\Exceptions\RepositoryException;
use PhpMqtt\Client\PendingMessage;
use PhpMqtt\Client\Subscription;
/**
* Implementations of this interface provide storage capabilities to an MQTT client.
*
* Services of this type have three primary goals:
* 1. Providing and keeping track of message identifiers, since they must be unique
* within the message flow (i.e. there may not be duplicates of different messages
* at the same time).
* 2. Storing and keeping track of subscriptions, which is especially necessary in case
* of persisted sessions.
* 3. Storing and keeping track of pending messages (i.e. sent messages, which have not
* been acknowledged yet by the broker).
*
* @package PhpMqtt\Client\Contracts
*/
interface Repository
{
/**
* Re-initializes the repository by deleting all persisted data and restoring the original state,
* which was given when the repository was first created. This is used when a clean session
* is requested by a client during connection.
*/
public function reset(): void;
/**
* Returns a new message id. The message id might have been used before,
* but it is currently not being used (i.e. in a resend queue).
*
* @throws RepositoryException
*/
public function newMessageId(): int;
/**
* Returns the number of pending outgoing messages.
*/
public function countPendingOutgoingMessages(): int;
/**
* Gets a pending outgoing message with the given message identifier, if found.
*/
public function getPendingOutgoingMessage(int $messageId): ?PendingMessage;
/**
* Gets a list of pending outgoing messages last sent before the given date time.
*
* If date time is `null`, all pending messages are returned.
*
* The messages are returned in the same order they were added to the repository.
*
* @return PendingMessage[]
*/
public function getPendingOutgoingMessagesLastSentBefore(?DateTime $dateTime = null): array;
/**
* Adds a pending outgoing message to the repository.
*
* @throws PendingMessageAlreadyExistsException
*/
public function addPendingOutgoingMessage(PendingMessage $message): void;
/**
* Marks an existing pending outgoing published message as received in the repository.
*
* If the message does not exists, an exception is thrown,
* otherwise `true` is returned if the message was marked as received, and `false`
* in case it was already marked as received.
*
* @throws PendingMessageNotFoundException
*/
public function markPendingOutgoingPublishedMessageAsReceived(int $messageId): bool;
/**
* Removes a pending outgoing message from the repository.
*
* If a pending message with the given identifier is found and
* successfully removed from the repository, `true` is returned.
* Otherwise `false` will be returned.
*/
public function removePendingOutgoingMessage(int $messageId): bool;
/**
* Returns the number of pending incoming messages.
*/
public function countPendingIncomingMessages(): int;
/**
* Gets a pending incoming message with the given message identifier, if found.
*/
public function getPendingIncomingMessage(int $messageId): ?PendingMessage;
/**
* Adds a pending outgoing message to the repository.
*
* @throws PendingMessageAlreadyExistsException
*/
public function addPendingIncomingMessage(PendingMessage $message): void;
/**
* Removes a pending incoming message from the repository.
*
* If a pending message with the given identifier is found and
* successfully removed from the repository, `true` is returned.
* Otherwise `false` will be returned.
*/
public function removePendingIncomingMessage(int $messageId): bool;
/**
* Returns the number of registered subscriptions.
*/
public function countSubscriptions(): int;
/**
* Adds a subscription to the repository.
*/
public function addSubscription(Subscription $subscription): void;
/**
* Gets all subscriptions matching the given topic.
*
* @return Subscription[]
*/
public function getSubscriptionsMatchingTopic(string $topicName): array;
/**
* Removes the subscription with the given topic filter from the repository.
*
* Returns `true` if a topic subscription existed and has been removed.
* Otherwise, `false` is returned.
*/
public function removeSubscription(string $topicFilter): bool;
}
@@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client is not connected to a broker and tries
* to perform an action which requires a connection (e.g. publish or subscribe).
*
* @package PhpMqtt\Client\Exceptions
*/
class ClientNotConnectedToBrokerException extends DataTransferException
{
public const EXCEPTION_CONNECTION_LOST = 0300;
/**
* ClientNotConnectedToBrokerException constructor.
*/
public function __construct(string $error)
{
parent::__construct(self::EXCEPTION_CONNECTION_LOST, $error);
}
}
@@ -1,15 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client has been misconfigured or wrong connection
* settings are being used.
*
* @package PhpMqtt\Client\Exceptions
*/
class ConfigurationInvalidException extends MqttClientException
{
}
@@ -1,54 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client could not connect to the broker.
*
* @package PhpMqtt\Client\Exceptions
*/
class ConnectingToBrokerFailedException extends MqttClientException
{
public const EXCEPTION_CONNECTION_FAILED = 0001;
public const EXCEPTION_CONNECTION_PROTOCOL_VERSION = 0002;
public const EXCEPTION_CONNECTION_IDENTIFIER_REJECTED = 0003;
public const EXCEPTION_CONNECTION_BROKER_UNAVAILABLE = 0004;
public const EXCEPTION_CONNECTION_INVALID_CREDENTIALS = 0005;
public const EXCEPTION_CONNECTION_UNAUTHORIZED = 0006;
public const EXCEPTION_CONNECTION_SOCKET_ERROR = 1000;
public const EXCEPTION_CONNECTION_TLS_ERROR = 2000;
/**
* ConnectingToBrokerFailedException constructor.
*/
public function __construct(
int $code,
string $error,
private ?string $connectionErrorCode = null,
private ?string $connectionErrorMessage = null,
)
{
parent::__construct(
sprintf('[%s] Establishing a connection to the MQTT broker failed: %s', $code, $error),
$code
);
}
/**
* Retrieves the connection error code.
*/
public function getConnectionErrorCode(): ?string
{
return $this->connectionErrorCode;
}
/**
* Retrieves the connection error message.
*/
public function getConnectionErrorMessage(): ?string
{
return $this->connectionErrorMessage;
}
}
@@ -1,27 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client encountered an error while transferring data.
*
* @package PhpMqtt\Client\Exceptions
*/
class DataTransferException extends MqttClientException
{
public const EXCEPTION_TX_DATA = 0101;
public const EXCEPTION_RX_DATA = 0102;
/**
* DataTransferException constructor.
*/
public function __construct(int $code, string $error)
{
parent::__construct(
sprintf('[%s] Transferring data over socket failed: %s', $code, $error),
$code
);
}
}
@@ -1,14 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client encounters an invalid message.
*
* @package PhpMqtt\Client\Exceptions
*/
class InvalidMessageException extends MqttClientException
{
}
@@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client error occurs.
*
* @package PhpMqtt\Client\Exceptions
*/
class MqttClientException extends \Exception
{
/**
* MqttClientException constructor.
*/
public function __construct(string $message = '', int $code = 0, ?\Throwable $parentException = null)
{
if (empty($message)) {
parent::__construct(
sprintf('[%s] The MQTT client encountered an error.', $code),
$code,
$parentException
);
} else {
parent::__construct($message, $code, $parentException);
}
}
}
@@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if a pending message with the same packet identifier is still pending.
*
* @package PhpMqtt\Client\Exceptions
*/
class PendingMessageAlreadyExistsException extends RepositoryException
{
/**
* PendingMessageAlreadyExistsException constructor.
*/
public function __construct(int $messageId)
{
parent::__construct(sprintf('A pending message with the message identifier [%s] exists already.', $messageId));
}
}
@@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if a pending message with the same packet identifier is not found.
*
* @package PhpMqtt\Client\Exceptions
*/
class PendingMessageNotFoundException extends RepositoryException
{
/**
* PendingMessageNotFoundException constructor.
*/
public function __construct(int $messageId)
{
parent::__construct(sprintf('No pending message with the message identifier [%s].', $messageId));
}
}
@@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an invalid MQTT version is given.
*
* @package PhpMqtt\Client\Exceptions
*/
class ProtocolNotSupportedException extends MqttClientException
{
/**
* ProtocolNotSupportedException constructor.
*/
public function __construct(string $protocol)
{
parent::__construct(sprintf('The given protocol version [%s] is not supported.', $protocol));
}
}
@@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client encountered a protocol violation.
*
* @package PhpMqtt\Client\Exceptions
*/
class ProtocolViolationException extends MqttClientException
{
/**
* ProtocolViolationException constructor.
*/
public function __construct(string $error)
{
parent::__construct(sprintf('Protocol violation: %s.', $error));
}
}
@@ -1,14 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client repository encounters an error.
*
* @package PhpMqtt\Client\Exceptions
*/
class RepositoryException extends MqttClientException
{
}
-166
View File
@@ -1,166 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
/**
* Wrapper for another logger. Drops logged messages if no logger is available.
*
* @internal This class is not part of the public API of the library and used internally only.
* @package PhpMqtt\Client
*/
class Logger implements LoggerInterface
{
/**
* Logger constructor.
*
* @param LoggerInterface|null $logger
*/
public function __construct(
private string $host,
private int $port,
private string $clientId,
private ?LoggerInterface $logger = null,
)
{
}
/**
* System is unusable.
*
* @param string $message
* @param array $context
*/
public function emergency($message, array $context = []): void
{
$this->log(LogLevel::EMERGENCY, $message, $context);
}
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string $message
* @param array $context
*/
public function alert($message, array $context = []): void
{
$this->log(LogLevel::ALERT, $message, $context);
}
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
* @param array $context
*/
public function critical($message, array $context = []): void
{
$this->log(LogLevel::CRITICAL, $message, $context);
}
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
* @param array $context
*/
public function error($message, array $context = []): void
{
$this->log(LogLevel::ERROR, $message, $context);
}
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string $message
* @param array $context
*/
public function warning($message, array $context = []): void
{
$this->log(LogLevel::WARNING, $message, $context);
}
/**
* Normal but significant events.
*
* @param string $message
* @param array $context
*/
public function notice($message, array $context = []): void
{
$this->log(LogLevel::NOTICE, $message, $context);
}
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message
* @param array $context
*/
public function info($message, array $context = []): void
{
$this->log(LogLevel::INFO, $message, $context);
}
/**
* Detailed debug information.
*
* @param string $message
* @param array $context
*/
public function debug($message, array $context = []): void
{
$this->log(LogLevel::DEBUG, $message, $context);
}
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param array $context
*/
public function log($level, $message, array $context = []): void
{
if ($this->logger === null) {
return;
}
$this->logger->log($level, $this->wrapLogMessage($message), $this->mergeContext($context));
}
/**
* Wraps the given log message by prepending the client id and broker.
*/
protected function wrapLogMessage(string $message): string
{
return 'MQTT [{host}:{port}] [{clientId}] ' . $message;
}
/**
* Adds global context like host, port and client id to the log context.
*/
protected function mergeContext(array $context): array
{
return array_merge([
'host' => $this->host,
'port' => $this->port,
'clientId' => $this->clientId,
], $context);
}
}
-105
View File
@@ -1,105 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client;
use PhpMqtt\Client\Contracts\MessageProcessor;
use PhpMqtt\Client\Contracts\MqttClient;
/**
* Describes an action which is supposed to be performed after receiving a message.
* Objects of this type are used by the {@see MessageProcessor} to instruct the
* {@see MqttClient} about required steps to take.
*
* @package PhpMqtt\Client
*/
class Message
{
private ?int $messageId = null;
private ?string $topic = null;
private ?string $content = null;
/** @var int[] */
private array $acknowledgedQualityOfServices = [];
/**
* Message constructor.
*/
public function __construct(
private MessageType $type,
private int $qualityOfService = 0,
private bool $retained = false,
)
{
}
public function getType(): MessageType
{
return $this->type;
}
public function getQualityOfService(): int
{
return $this->qualityOfService;
}
public function getRetained(): bool
{
return $this->retained;
}
public function getMessageId(): ?int
{
return $this->messageId;
}
public function setMessageId(?int $messageId): Message
{
$this->messageId = $messageId;
return $this;
}
public function getTopic(): ?string
{
return $this->topic;
}
public function setTopic(?string $topic): Message
{
$this->topic = $topic;
return $this;
}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(?string $content): Message
{
$this->content = $content;
return $this;
}
/**
* @return int[]
*/
public function getAcknowledgedQualityOfServices(): array
{
return $this->acknowledgedQualityOfServices;
}
/**
* @param int[] $acknowledgedQualityOfServices
*/
public function setAcknowledgedQualityOfServices(array $acknowledgedQualityOfServices): Message
{
$this->acknowledgedQualityOfServices = $acknowledgedQualityOfServices;
return $this;
}
}
@@ -1,32 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\MessageProcessors;
use PhpMqtt\Client\Concerns\TranscodesData;
use PhpMqtt\Client\Concerns\WorksWithBuffers;
use Psr\Log\LoggerInterface;
/**
* This message processor serves as base for other message processors, providing
* default implementations for some methods.
*
* @package PhpMqtt\Client\MessageProcessors
*/
abstract class BaseMessageProcessor
{
use TranscodesData;
use WorksWithBuffers;
public const QOS_AT_MOST_ONCE = 0;
public const QOS_AT_LEAST_ONCE = 1;
public const QOS_EXACTLY_ONCE = 2;
/**
* BaseMessageProcessor constructor.
*/
public function __construct(protected LoggerInterface $logger)
{
}
}
@@ -1,76 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\MessageProcessors;
use PhpMqtt\Client\Exceptions\InvalidMessageException;
use PhpMqtt\Client\Exceptions\ProtocolViolationException;
use PhpMqtt\Client\Message;
use PhpMqtt\Client\MessageType;
/**
* This message processor implements the MQTT protocol version 3.1.1.
*
* @package PhpMqtt\Client\MessageProcessors
*/
class Mqtt311MessageProcessor extends Mqtt31MessageProcessor
{
/**
* {@inheritDoc}
*/
protected function getEncodedProtocolNameAndVersion(): string
{
return $this->buildLengthPrefixedString('MQTT') . chr(0x04); // protocol version (4)
}
/**
* {@inheritDoc}
*/
public function parseAndValidateMessage(string $message): ?Message
{
$result = parent::parseAndValidateMessage($message);
if ($this->isPublishMessageWithNullCharacter($result)) {
throw new ProtocolViolationException('The broker sent us a message with the forbidden unicode character U+0000.');
}
return $result;
}
/**
* {@inheritDoc}
*/
protected function parseAndValidateSubscribeAcknowledgementMessage(string $data): Message
{
if (strlen($data) < 3) {
$this->logger->notice('Received invalid subscribe acknowledgement from the broker.');
throw new InvalidMessageException('Received invalid subscribe acknowledgement from the broker.');
}
$messageId = $this->decodeMessageId($this->pop($data, 2));
// Parse and validate the QoS acknowledgements.
$acknowledgements = array_map('ord', str_split($data));
foreach ($acknowledgements as $acknowledgement) {
if (!in_array($acknowledgement, [0, 1, 2, 128])) {
throw new InvalidMessageException('Received subscribe acknowledgement with invalid QoS values from the broker.');
}
}
return (new Message(MessageType::SUBSCRIBE_ACKNOWLEDGEMENT()))
->setMessageId($messageId)
->setAcknowledgedQualityOfServices($acknowledgements);
}
/**
* Determines if the given message is a PUBLISH message and contains the unicode null character U+0000.
*/
private function isPublishMessageWithNullCharacter(?Message $message): bool
{
return $message !== null
&& $message->getType()->equals(MessageType::PUBLISH())
&& $message->getContent() !== null
&& preg_match('/\x{0000}/u', $message->getContent());
}
}
@@ -1,712 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\MessageProcessors;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\Contracts\MessageProcessor;
use PhpMqtt\Client\Exceptions\ConnectingToBrokerFailedException;
use PhpMqtt\Client\Exceptions\InvalidMessageException;
use PhpMqtt\Client\Exceptions\ProtocolViolationException;
use PhpMqtt\Client\Message;
use PhpMqtt\Client\MessageType;
use Psr\Log\LoggerInterface;
/**
* This message processor implements the MQTT protocol version 3.1.
*
* @package PhpMqtt\Client\MessageProcessors
*/
class Mqtt31MessageProcessor extends BaseMessageProcessor implements MessageProcessor
{
/**
* Creates a new message processor instance which supports version 3.1 of the MQTT protocol.
*/
public function __construct(private string $clientId, LoggerInterface $logger)
{
parent::__construct($logger);
}
/**
* {@inheritDoc}
*/
public function tryFindMessageInBuffer(string $buffer, int $bufferLength, ?string &$message = null, int &$requiredBytes = -1): bool
{
// If we received no input, we can return immediately without doing work.
if ($bufferLength === 0) {
return false;
}
// If we received not at least the fixed header with one length indicating byte,
// we know that there can't be a valid message in the buffer. So we return early.
if ($bufferLength < 2) {
return false;
}
// Read the second byte of the message to get the remaining length.
// If the continuation bit (8) is set on the length byte, another byte will be read as length.
$byteIndex = 1;
$remainingLength = 0;
$multiplier = 1;
do {
// If the buffer has no more data, but we need to read more for the length header,
// we cannot give useful information about the remaining length and exit early.
if ($byteIndex + 1 > $bufferLength) {
return false;
}
// There can me a maximum of four bytes for the package length, which means we cann opt-out
// when reaching the 6th byte in the buffer. This is only a safety measure in case the broker
// is sending invalid messages. Normally, the loop exits on its own.
if ($byteIndex >= 6) {
break;
}
// Otherwise, we can take seven bits to calculate the length and the remaining eighth bit
// as continuation bit.
$digit = ord($buffer[$byteIndex]);
$remainingLength += ($digit & 127) * $multiplier;
$multiplier *= 128;
$byteIndex++;
} while (($digit & 128) !== 0);
// At this point, we can now tell whether the remaining length amount of bytes are available
// or not. If not, we return the amount of bytes required for the message to be complete.
$requiredBufferLength = $byteIndex + $remainingLength;
if ($requiredBufferLength > $bufferLength) {
$requiredBytes = $requiredBufferLength;
return false;
}
// Now that we have a full message in the buffer, we can set the output and return.
$message = substr($buffer, 0, $requiredBufferLength);
return true;
}
/**
* {@inheritDoc}
*/
public function buildConnectMessage(ConnectionSettings $connectionSettings, bool $useCleanSession = false): string
{
// The protocol name and version.
$buffer = $this->getEncodedProtocolNameAndVersion();
// Build connection flags based on the connection settings.
$buffer .= chr($this->buildConnectionFlags($connectionSettings, $useCleanSession));
// Encode and add the keep alive interval.
$buffer .= chr($connectionSettings->getKeepAliveInterval() >> 8);
$buffer .= chr($connectionSettings->getKeepAliveInterval() & 0xff);
// Encode and add the client identifier.
$buffer .= $this->buildLengthPrefixedString($this->clientId);
// Encode and add the last will topic and message, if configured.
if ($connectionSettings->hasLastWill()) {
$buffer .= $this->buildLengthPrefixedString($connectionSettings->getLastWillTopic());
$buffer .= $this->buildLengthPrefixedString($connectionSettings->getLastWillMessage());
}
// Encode and add the credentials, if configured.
if ($connectionSettings->getUsername() !== null) {
$buffer .= $this->buildLengthPrefixedString($connectionSettings->getUsername());
}
if ($connectionSettings->getPassword() !== null) {
$buffer .= $this->buildLengthPrefixedString($connectionSettings->getPassword());
}
// The header consists of the message type 0x10 and the length.
$header = chr(0x10) . $this->encodeMessageLength(strlen($buffer));
return $header . $buffer;
}
/**
* Returns the encoded protocol name and version, ready to be sent as part of the CONNECT message.
*/
protected function getEncodedProtocolNameAndVersion(): string
{
return $this->buildLengthPrefixedString('MQIsdp') . chr(0x03); // protocol version (3)
}
/**
* Builds the connection flags from the inputs and settings.
*
* The bit structure of the connection flags is as follows:
* 0 - reserved
* 1 - clean session flag
* 2 - last will flag
* 3 - QoS flag (1)
* 4 - QoS flag (2)
* 5 - retain last will flag
* 6 - password flag
* 7 - username flag
*
* @link http://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/mqtt-v3r1.html#connect MQTT 3.1 Spec
*/
protected function buildConnectionFlags(ConnectionSettings $connectionSettings, bool $useCleanSession = false): int
{
$flags = 0;
if ($useCleanSession) {
$this->logger->debug('Using the [clean session] flag for the connection.');
$flags += 1 << 1;
}
if ($connectionSettings->hasLastWill()) {
$this->logger->debug('Using the [will] flag for the connection.');
$flags += 1 << 2;
if ($connectionSettings->getLastWillQualityOfService() > self::QOS_AT_MOST_ONCE) {
$this->logger->debug('Using last will QoS level [{qos}] for the connection.', [
'qos' => $connectionSettings->getLastWillQualityOfService(),
]);
$flags += $connectionSettings->getLastWillQualityOfService() << 3;
}
if ($connectionSettings->shouldRetainLastWill()) {
$this->logger->debug('Using the [retain last will] flag for the connection.');
$flags += 1 << 5;
}
}
if ($connectionSettings->getPassword() !== null) {
$this->logger->debug('Using the [password] flag for the connection.');
$flags += 1 << 6;
}
if ($connectionSettings->getUsername() !== null) {
$this->logger->debug('Using the [username] flag for the connection.');
$flags += 1 << 7;
}
return $flags;
}
/**
* {@inheritDoc}
*/
public function handleConnectAcknowledgement(string $message): void
{
if (strlen($message) !== 4 || ($messageType = ord($message[0]) >> 4) !== 2) {
$this->logger->error('Expected connect acknowledgement; received a different response.', ['messageType' => $messageType ?? null]);
throw new ConnectingToBrokerFailedException(
ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_FAILED,
'A connection could not be established. Expected connect acknowledgement; received a different response else.'
);
}
$errorCode = ord($message[3]);
$logContext = ['errorCode' => sprintf('0x%02X', $errorCode)];
switch ($errorCode) {
case 0x00:
$this->logger->info('Connection with broker established successfully.', $logContext);
break;
case 0x01:
$this->logger->error('The broker does not support MQTT v3.1.', $logContext);
throw new ConnectingToBrokerFailedException(
ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_PROTOCOL_VERSION,
'The configured broker does not support MQTT v3.1.'
);
case 0x02:
$this->logger->error('The broker rejected the sent identifier.', $logContext);
throw new ConnectingToBrokerFailedException(
ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_IDENTIFIER_REJECTED,
'The configured broker rejected the sent identifier.'
);
case 0x03:
$this->logger->error('The broker is currently unavailable.', $logContext);
throw new ConnectingToBrokerFailedException(
ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_BROKER_UNAVAILABLE,
'The configured broker is currently unavailable.'
);
case 0x04:
$this->logger->error('The broker reported the credentials as invalid.', $logContext);
throw new ConnectingToBrokerFailedException(
ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_INVALID_CREDENTIALS,
'The configured broker reported the credentials as invalid.'
);
case 0x05:
$this->logger->error('The broker responded with unauthorized.', $logContext);
throw new ConnectingToBrokerFailedException(
ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_UNAUTHORIZED,
'The configured broker responded with unauthorized.'
);
default:
$this->logger->error('The broker responded with an invalid error code [{errorCode}].', $logContext);
throw new ConnectingToBrokerFailedException(
ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_FAILED,
'The configured broker responded with an invalid error code. A connection could not be established.'
);
}
}
/**
* Builds a ping request message.
*/
public function buildPingRequestMessage(): string
{
// The message consists of the command 0xc0 and the length 0.
return chr(0xc0) . chr(0x00);
}
/**
* Builds a ping response message.
*/
public function buildPingResponseMessage(): string
{
// The message consists of the command 0xd0 and the length 0.
return chr(0xd0) . chr(0x00);
}
/**
* Builds a disconnect message.
*/
public function buildDisconnectMessage(): string
{
// The message consists of the command 0xe0 and the length 0.
return chr(0xe0) . chr(0x00);
}
/**
* {@inheritDoc}
*/
public function buildSubscribeMessage(int $messageId, array $subscriptions, bool $isDuplicate = false): string
{
// Encode the message id, it always consists of two bytes.
$buffer = $this->encodeMessageId($messageId);
foreach ($subscriptions as $subscription) {
// Encode the topic as length prefixed string.
$buffer .= $this->buildLengthPrefixedString($subscription->getTopicFilter());
// Encode the quality of service level.
$buffer .= chr($subscription->getQualityOfServiceLevel());
}
// The header consists of the message type 0x82 and the length.
$header = chr(0x82) . $this->encodeMessageLength(strlen($buffer));
return $header . $buffer;
}
/**
* {@inheritDoc}
*/
public function buildUnsubscribeMessage(int $messageId, array $topics, bool $isDuplicate = false): string
{
// Encode the message id, it always consists of two bytes.
$buffer = $this->encodeMessageId($messageId);
foreach ($topics as $topic) {
// Encode the topic as length prefixed string.
$buffer .= $this->buildLengthPrefixedString($topic);
}
// The header consists of the message type 0xa2 and the length.
// Additionally, the first byte may contain the duplicate flag.
$command = 0xa2 | ($isDuplicate ? 1 << 3 : 0);
$header = chr($command) . $this->encodeMessageLength(strlen($buffer));
return $header . $buffer;
}
/**
* {@inheritDoc}
*/
public function buildPublishMessage(
string $topic,
string $message,
int $qualityOfService,
bool $retain,
?int $messageId = null,
bool $isDuplicate = false,
): string
{
// Encode the topic as length prefixed string.
$buffer = $this->buildLengthPrefixedString($topic);
// Encode the message id, if given. It always consists of two bytes.
if ($messageId !== null)
{
$buffer .= $this->encodeMessageId($messageId);
}
// Add the message without encoding.
$buffer .= $message;
// Encode the command with supported flags.
$command = 0x30;
if ($retain) {
$command += 1 << 0;
}
if ($qualityOfService > self::QOS_AT_MOST_ONCE) {
$command += $qualityOfService << 1;
}
if ($qualityOfService > self::QOS_AT_MOST_ONCE && $isDuplicate) {
$command += 1 << 3;
}
// Build the header from the command and the encoded message length.
$header = chr($command) . $this->encodeMessageLength(strlen($buffer));
return $header . $buffer;
}
/**
* {@inheritDoc}
*/
public function buildPublishAcknowledgementMessage(int $messageId): string
{
return chr(0x40) . chr(0x02) . $this->encodeMessageId($messageId);
}
/**
* {@inheritDoc}
*/
public function buildPublishReceivedMessage(int $messageId): string
{
return chr(0x50) . chr(0x02) . $this->encodeMessageId($messageId);
}
/**
* {@inheritDoc}
*/
public function buildPublishReleaseMessage(int $messageId): string
{
return chr(0x62) . chr(0x02) . $this->encodeMessageId($messageId);
}
/**
* {@inheritDoc}
*/
public function buildPublishCompleteMessage(int $messageId): string
{
return chr(0x70) . chr(0x02) . $this->encodeMessageId($messageId);
}
/**
* {@inheritDoc}
*/
public function parseAndValidateMessage(string $message): ?Message
{
$qualityOfService = 0;
$retained = false;
$data = '';
$result = $this->tryDecodeMessage($message, $command, $qualityOfService, $retained, $data);
if ($result === false) {
throw new InvalidMessageException('The passed message could not be decoded.');
}
// Ensure the command is supported by this version of the protocol.
if ($command <= 0 || $command >= 15) {
$this->logger->error('Reserved command received from the broker. Supported are commands (including) 1-14.', [
'command' => $command,
]);
throw new InvalidMessageException('A reserved command has been used in the message.');
}
// Then handle the command accordingly.
switch ($command) {
case 0x02:
throw new ProtocolViolationException('Unexpected connection acknowledgement.');
case 0x03:
return $this->parseAndValidatePublishMessage($data, $qualityOfService, $retained);
case 0x04:
return $this->parseAndValidatePublishAcknowledgementMessage($data);
case 0x05:
return $this->parseAndValidatePublishReceiptMessage($data);
case 0x06:
return $this->parseAndValidatePublishReleaseMessage($data);
case 0x07:
return $this->parseAndValidatePublishCompleteMessage($data);
case 0x09:
return $this->parseAndValidateSubscribeAcknowledgementMessage($data);
case 0x0b:
return $this->parseAndValidateUnsubscribeAcknowledgementMessage($data);
case 0x0c:
return $this->parseAndValidatePingRequestMessage();
case 0x0d:
return $this->parseAndValidatePingAcknowledgementMessage();
default:
$this->logger->debug('Received message with unsupported command [{command}]. Skipping.', ['command' => $command]);
break;
}
// If we arrive here, we must have parsed a message with an unsupported type, and it cannot be
// very relevant for us. So we return an empty result without information to skip processing.
return null;
}
/**
* Attempt to decode the given message. If successful, the result is true and the reference
* parameters are set accordingly. Otherwise, false is returned and the reference parameters
* remain untouched.
*/
protected function tryDecodeMessage(
string $message,
?int &$command = null,
?int &$qualityOfService = null,
?bool &$retained = null,
?string &$data = null
): bool
{
// If we received no input, we can return immediately without doing work.
if (strlen($message) === 0) {
return false;
}
// If we received not at least the fixed header with one length indicating byte,
// we know that there can't be a valid message in the buffer. So we return early.
if (strlen($message) < 2) {
return false;
}
// Read the first byte of a message (command and flags).
$byte = $message[0];
$command = (int) (ord($byte) / 16);
$qualityOfService = (ord($byte) & 0x06) >> 1;
$retained = (bool) (ord($byte) & 0x01);
// Read the second byte of a message (remaining length).
// If the continuation bit (8) is set on the length byte, another byte will be read as length.
$byteIndex = 1;
$remainingLength = 0;
$multiplier = 1;
do {
// If the buffer has no more data, but we need to read more for the length header,
// we cannot give useful information about the remaining length and exit early.
if ($byteIndex + 1 > strlen($message)) {
return false;
}
// Otherwise, we can take seven bits to calculate the length and the remaining eighth bit
// as continuation bit.
$digit = ord($message[$byteIndex]);
$remainingLength += ($digit & 127) * $multiplier;
$multiplier *= 128;
$byteIndex++;
} while (($digit & 128) !== 0);
// At this point, we can now tell whether the remaining length amount of bytes are available
// or not. If not, the message is incomplete.
$requiredBytes = $byteIndex + $remainingLength;
if ($requiredBytes > strlen($message)) {
return false;
}
// Set the output data based on the calculated bytes.
$data = substr($message, $byteIndex, $remainingLength);
return true;
}
/**
* Parses a received published message. The data contains the whole message except the
* fixed header with command and length. The message structure is:
*
* [topic-length:topic:message]+
*/
protected function parseAndValidatePublishMessage(string $data, int $qualityOfServiceLevel, bool $retained): ?Message
{
$topicLength = (ord($data[0]) << 8) + ord($data[1]);
$topic = substr($data, 2, $topicLength);
$content = substr($data, ($topicLength + 2));
$message = new Message(MessageType::PUBLISH(), $qualityOfServiceLevel, $retained);
if ($qualityOfServiceLevel > self::QOS_AT_MOST_ONCE) {
if (strlen($content) < 2) {
$this->logger->error('Received a message with QoS level [{qos}] without message identifier. Waiting for retransmission.', [
'qos' => $qualityOfServiceLevel,
]);
// This message seems to be incomplete or damaged. We ignore it and wait for a retransmission,
// which will occur at some point due to QoS level > 0.
return null;
}
// Publish messages with a quality of service level > 0 require acknowledgement and therefore
// also a message identifier.
$messageId = $this->decodeMessageId($this->pop($content, 2));
$message->setMessageId($messageId);
}
return $message
->setTopic($topic)
->setContent($content);
}
/**
* Parses a received publish acknowledgement. The data contains the whole message except
* the fixed header with command and length. The message structure is:
*
* [message-identifier]
*
* @throws InvalidMessageException
*/
protected function parseAndValidatePublishAcknowledgementMessage(string $data): Message
{
if (strlen($data) !== 2) {
$this->logger->notice('Received invalid publish acknowledgement from the broker.');
throw new InvalidMessageException('Received invalid publish acknowledgement from the broker.');
}
$messageId = $this->decodeMessageId($this->pop($data, 2));
return (new Message(MessageType::PUBLISH_ACKNOWLEDGEMENT()))
->setMessageId($messageId);
}
/**
* Parses a received publish receipt. The data contains the whole message except the
* fixed header with command and length. The message structure is:
*
* [message-identifier]
*
* @throws InvalidMessageException
*/
protected function parseAndValidatePublishReceiptMessage(string $data): Message
{
if (strlen($data) !== 2) {
$this->logger->notice('Received invalid publish receipt from the broker.');
throw new InvalidMessageException('Received invalid publish receipt from the broker.');
}
$messageId = $this->decodeMessageId($this->pop($data, 2));
return (new Message(MessageType::PUBLISH_RECEIPT()))
->setMessageId($messageId);
}
/**
* Parses a received publish release message. The data contains the whole message except the
* fixed header with command and length. The message structure is:
*
* [message-identifier]
*
* @throws InvalidMessageException
*/
protected function parseAndValidatePublishReleaseMessage(string $data): Message
{
if (strlen($data) !== 2) {
$this->logger->notice('Received invalid publish release from the broker.');
throw new InvalidMessageException('Received invalid publish release from the broker.');
}
$messageId = $this->decodeMessageId($this->pop($data, 2));
return (new Message(MessageType::PUBLISH_RELEASE()))
->setMessageId($messageId);
}
/**
* Parses a received publish confirmation message. The data contains the whole message except the
* fixed header with command and length. The message structure is:
*
* [message-identifier]
*
* @throws InvalidMessageException
*/
protected function parseAndValidatePublishCompleteMessage(string $data): Message
{
if (strlen($data) !== 2) {
$this->logger->notice('Received invalid publish complete from the broker.');
throw new InvalidMessageException('Received invalid complete release from the broker.');
}
$messageId = $this->decodeMessageId($this->pop($data, 2));
return (new Message(MessageType::PUBLISH_COMPLETE()))
->setMessageId($messageId);
}
/**
* Parses a received subscription acknowledgement. The data contains the whole message except the
* fixed header with command and length. The message structure is:
*
* [message-identifier:[qos-level]+]
*
* The order of the received QoS levels matches the order of the sent subscriptions.
*
* @throws InvalidMessageException
*/
protected function parseAndValidateSubscribeAcknowledgementMessage(string $data): Message
{
if (strlen($data) < 3) {
$this->logger->notice('Received invalid subscribe acknowledgement from the broker.');
throw new InvalidMessageException('Received invalid subscribe acknowledgement from the broker.');
}
$messageId = $this->decodeMessageId($this->pop($data, 2));
// Parse and validate the QoS acknowledgements.
$acknowledgements = array_map('ord', str_split($data));
foreach ($acknowledgements as $acknowledgement) {
if (!in_array($acknowledgement, [0, 1, 2])) {
throw new InvalidMessageException('Received subscribe acknowledgement with invalid QoS values from the broker.');
}
}
return (new Message(MessageType::SUBSCRIBE_ACKNOWLEDGEMENT()))
->setMessageId($messageId)
->setAcknowledgedQualityOfServices($acknowledgements);
}
/**
* Parses a received unsubscribe acknowledgement. The data contains the whole message except the
* fixed header with command and length. The message structure is:
*
* [message-identifier]
*
* @throws InvalidMessageException
*/
protected function parseAndValidateUnsubscribeAcknowledgementMessage(string $data): Message
{
if (strlen($data) !== 2) {
$this->logger->notice('Received invalid unsubscribe acknowledgement from the broker.');
throw new InvalidMessageException('Received invalid unsubscribe acknowledgement from the broker.');
}
$messageId = $this->decodeMessageId($this->pop($data, 2));
return (new Message(MessageType::UNSUBSCRIBE_ACKNOWLEDGEMENT()))
->setMessageId($messageId);
}
/**
* Parses a received ping request.
*/
protected function parseAndValidatePingRequestMessage(): Message
{
return new Message(MessageType::PING_REQUEST());
}
/**
* Parses a received ping acknowledgement.
*/
protected function parseAndValidatePingAcknowledgementMessage(): Message
{
return new Message(MessageType::PING_RESPONSE());
}
}
-37
View File
@@ -1,37 +0,0 @@
<?php
/** @noinspection PhpUnusedPrivateFieldInspection */
declare(strict_types=1);
namespace PhpMqtt\Client;
use MyCLabs\Enum\Enum;
/**
* An enumeration describing types of messages.
*
* @method static MessageType PUBLISH()
* @method static MessageType PUBLISH_ACKNOWLEDGEMENT()
* @method static MessageType PUBLISH_RECEIPT()
* @method static MessageType PUBLISH_RELEASE()
* @method static MessageType PUBLISH_COMPLETE()
* @method static MessageType SUBSCRIBE_ACKNOWLEDGEMENT()
* @method static MessageType UNSUBSCRIBE_ACKNOWLEDGEMENT()
* @method static MessageType PING_REQUEST()
* @method static MessageType PING_RESPONSE()
*
* @package PhpMqtt\Client
*/
class MessageType extends Enum
{
private const PUBLISH = 'PUBLISH';
private const PUBLISH_ACKNOWLEDGEMENT = 'PUBACK';
private const PUBLISH_RECEIPT = 'PUBREC';
private const PUBLISH_RELEASE = 'PUBREL';
private const PUBLISH_COMPLETE = 'PUBCOMP';
private const SUBSCRIBE_ACKNOWLEDGEMENT = 'SUBACK';
private const UNSUBSCRIBE_ACKNOWLEDGEMENT = 'UNSUBACK';
private const PING_REQUEST = 'PINGREQ';
private const PING_RESPONSE = 'PINGRESP';
}
File diff suppressed because it is too large Load Diff
-76
View File
@@ -1,76 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client;
use DateTime;
/**
* Represents a pending message.
*
* For messages with QoS 1 and 2 the client is responsible to resend the message if no
* acknowledgement is received from the broker within a given time period.
*
* This class serves as common base for message objects which need to be resent if no
* acknowledgement is received.
*
* @package PhpMqtt\Client
*/
abstract class PendingMessage
{
private int $sendingAttempts = 1;
private DateTime $lastSentAt;
/**
* Creates a new pending message object.
*/
protected function __construct(private int $messageId, ?DateTime $sentAt = null)
{
$this->lastSentAt = $sentAt ?? new DateTime();
}
/**
* Returns the message identifier.
*/
public function getMessageId(): int
{
return $this->messageId;
}
/**
* Returns the date time when the message was last sent.
*/
public function getLastSentAt(): DateTime
{
return $this->lastSentAt;
}
/**
* Returns the number of times the message has been sent.
*/
public function getSendingAttempts(): int
{
return $this->sendingAttempts;
}
/**
* Sets the date time when the message was last sent.
*/
public function setLastSentAt(?DateTime $value = null): self
{
$this->lastSentAt = $value ?? new DateTime();
return $this;
}
/**
* Increments the sending attempts by one.
*/
public function incrementSendingAttempts(): self
{
$this->sendingAttempts++;
return $this;
}
}
-84
View File
@@ -1,84 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client;
/**
* A simple DTO for published messages which need to be stored in a repository
* while waiting for the confirmation to be deliverable.
*
* @package PhpMqtt\Client
*/
class PublishedMessage extends PendingMessage
{
private bool $received = false;
/**
* Creates a new published message object.
*/
public function __construct(
int $messageId,
private string $topicName,
private string $message,
private int $qualityOfService,
private bool $retain,
)
{
parent::__construct($messageId);
}
/**
* Returns the topic name of the published message.
*/
public function getTopicName(): string
{
return $this->topicName;
}
/**
* Returns the content of the published message.
*/
public function getMessage(): string
{
return $this->message;
}
/**
* Returns the requested quality of service level.
*/
public function getQualityOfServiceLevel(): int
{
return $this->qualityOfService;
}
/**
* Determines whether this message wants to be retained.
*/
public function wantsToBeRetained(): bool
{
return $this->retain;
}
/**
* Determines whether the message has been confirmed as received.
*/
public function hasBeenReceived(): bool
{
return $this->received;
}
/**
* Marks the published message as received (QoS level 2).
*
* Returns `true` if the message was not previously received. Otherwise `false` will be returned.
*/
public function markAsReceived(): bool
{
$result = !$this->received;
$this->received = true;
return $result;
}
}
@@ -1,232 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Repositories;
use PhpMqtt\Client\Contracts\Repository;
use PhpMqtt\Client\Exceptions\PendingMessageAlreadyExistsException;
use PhpMqtt\Client\Exceptions\PendingMessageNotFoundException;
use PhpMqtt\Client\Exceptions\RepositoryException;
use PhpMqtt\Client\PendingMessage;
use PhpMqtt\Client\PublishedMessage;
use PhpMqtt\Client\Subscription;
/**
* Provides an in-memory implementation which manages message ids, subscriptions and pending messages.
* Instances of this type do not persist any data and are only meant for simple uses cases.
*
* @package PhpMqtt\Client\Repositories
*/
class MemoryRepository implements Repository
{
private int $nextMessageId = 1;
/** @var array<int, PendingMessage> */
private array $pendingOutgoingMessages = [];
/** @var array<int, PendingMessage> */
private array $pendingIncomingMessages = [];
/** @var array<int, Subscription> */
private array $subscriptions = [];
/**
* {@inheritDoc}
*/
public function reset(): void
{
$this->nextMessageId = 1;
$this->pendingOutgoingMessages = [];
$this->pendingIncomingMessages = [];
$this->subscriptions = [];
}
/**
* {@inheritDoc}
*/
public function newMessageId(): int
{
if (count($this->pendingOutgoingMessages) >= 65535) {
// This should never happen, as the server receive queue is
// normally smaller than the actual total number of message ids.
// Also, when using MQTT 5.0 the server can specify a smaller
// receive queue size (mosquitto for example has 20 by default),
// so the client has to implement the logic to honor this
// restriction and fallback to the protocol limit.
throw new RepositoryException('No more message identifiers available. The queue is full.');
}
while (isset($this->pendingOutgoingMessages[$this->nextMessageId])) {
$this->nextMessageId++;
if ($this->nextMessageId > 65535) {
$this->nextMessageId = 1;
}
}
return $this->nextMessageId;
}
/**
* {@inheritDoc}
*/
public function countPendingOutgoingMessages(): int
{
return count($this->pendingOutgoingMessages);
}
/**
* {@inheritDoc}
*/
public function getPendingOutgoingMessage(int $messageId): ?PendingMessage
{
return $this->pendingOutgoingMessages[$messageId] ?? null;
}
/**
* {@inheritDoc}
*/
public function getPendingOutgoingMessagesLastSentBefore(?\DateTime $dateTime = null): array
{
$result = [];
foreach ($this->pendingOutgoingMessages as $pendingMessage) {
if ($pendingMessage->getLastSentAt() < $dateTime) {
$result[] = $pendingMessage;
}
}
return $result;
}
/**
* {@inheritDoc}
*/
public function addPendingOutgoingMessage(PendingMessage $message): void
{
if (isset($this->pendingOutgoingMessages[$message->getMessageId()])) {
throw new PendingMessageAlreadyExistsException($message->getMessageId());
}
$this->pendingOutgoingMessages[$message->getMessageId()] = $message;
}
/**
* {@inheritDoc}
*/
public function markPendingOutgoingPublishedMessageAsReceived(int $messageId): bool
{
if (!isset($this->pendingOutgoingMessages[$messageId]) ||
!$this->pendingOutgoingMessages[$messageId] instanceof PublishedMessage) {
throw new PendingMessageNotFoundException($messageId);
}
return $this->pendingOutgoingMessages[$messageId]->markAsReceived();
}
/**
* {@inheritDoc}
*/
public function removePendingOutgoingMessage(int $messageId): bool
{
if (!isset($this->pendingOutgoingMessages[$messageId])) {
return false;
}
unset($this->pendingOutgoingMessages[$messageId]);
return true;
}
/**
* {@inheritDoc}
*/
public function countPendingIncomingMessages(): int
{
return count($this->pendingIncomingMessages);
}
/**
* {@inheritDoc}
*/
public function getPendingIncomingMessage(int $messageId): ?PendingMessage
{
return $this->pendingIncomingMessages[$messageId] ?? null;
}
/**
* {@inheritDoc}
*/
public function addPendingIncomingMessage(PendingMessage $message): void
{
if (isset($this->pendingIncomingMessages[$message->getMessageId()])) {
throw new PendingMessageAlreadyExistsException($message->getMessageId());
}
$this->pendingIncomingMessages[$message->getMessageId()] = $message;
}
/**
* {@inheritDoc}
*/
public function removePendingIncomingMessage(int $messageId): bool
{
if (!isset($this->pendingIncomingMessages[$messageId])) {
return false;
}
unset($this->pendingIncomingMessages[$messageId]);
return true;
}
/**
* {@inheritDoc}
*/
public function countSubscriptions(): int
{
return count($this->subscriptions);
}
/**
* {@inheritDoc}
*/
public function addSubscription(Subscription $subscription): void
{
// Remove a potentially existing subscription for this topic filter.
$this->removeSubscription($subscription->getTopicFilter());
$this->subscriptions[] = $subscription;
}
/**
* {@inheritDoc}
*/
public function getSubscriptionsMatchingTopic(string $topicName): array
{
$result = [];
foreach ($this->subscriptions as $subscription) {
if (!$subscription->matchesTopic($topicName)) {
continue;
}
$result[] = $subscription;
}
return $result;
}
/**
* {@inheritDoc}
*/
public function removeSubscription(string $topicFilter): bool
{
foreach ($this->subscriptions as $index => $subscription) {
if ($subscription->getTopicFilter() === $topicFilter) {
unset($this->subscriptions[$index]);
return true;
}
}
return false;
}
}
-38
View File
@@ -1,38 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client;
/**
* Represents a pending subscribe request.
*
* @package PhpMqtt\Client
*/
class SubscribeRequest extends PendingMessage
{
/** @var Subscription[] */
private array $subscriptions;
/**
* Creates a new subscribe request message.
*
* @param Subscription[] $subscriptions
*/
public function __construct(int $messageId, array $subscriptions)
{
parent::__construct($messageId);
$this->subscriptions = array_values($subscriptions);
}
/**
* Returns the subscriptions in this request.
*
* @return Subscription[]
*/
public function getSubscriptions(): array
{
return $this->subscriptions;
}
}
-106
View File
@@ -1,106 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client;
/**
* A simple DTO for subscriptions to a topic which need to be stored in a repository.
*
* @package PhpMqtt\Client
*/
class Subscription
{
private string $regexifiedTopicFilter;
/**
* Creates a new subscription object.
*/
public function __construct(
private string $topicFilter,
private int $qualityOfService = 0,
private ?\Closure $callback = null,
)
{
$this->regexifyTopicFilter();
}
/**
* Converts the topic filter into a regular expression.
*/
private function regexifyTopicFilter(): void
{
$topicFilter = $this->topicFilter;
// If the topic filter is for a shared subscription, we remove the shared subscription prefix as well as the group name
// from the topic filter. To do so, we look for the $share keyword and then try to find the second topic separator to
// calculate the substring containing the actual topic filter.
// Note: shared subscriptions always have the form: $share/<group>/<topic>
if (str_starts_with($topicFilter, '$share/') && ($separatorIndex = strpos($topicFilter, '/', 7)) !== false) {
$topicFilter = substr($topicFilter, $separatorIndex + 1);
}
$this->regexifiedTopicFilter = '/^' . str_replace(['$', '/', '+', '#'], ['\$', '\/', '([^\/]*)', '(.*)'], $topicFilter) . '$/';
}
/**
* Returns the topic of the subscription.
*/
public function getTopicFilter(): string
{
return $this->topicFilter;
}
/**
* Matches the given topic name matches to the subscription's topic filter.
*/
public function matchesTopic(string $topicName): bool
{
return (bool) preg_match($this->regexifiedTopicFilter, $topicName);
}
/**
* Returns an array which contains all matched wildcards of this subscription, taken from the given topic name.
*
* Example:
* Subscription topic filter: foo/+/bar/+/baz/#
* Result for 'foo/1/bar/2/baz': ['1', '2']
* Result for 'foo/my/bar/subscription/baz/42': ['my', 'subscription', '42']
* Result for 'foo/my/bar/subscription/baz/hello/world/123': ['my', 'subscription', 'hello', 'world', '123']
* Result for invalid topic 'some/topic': []
*
* Note: This method should only be called if {@see matchesTopic} returned true. An empty array will be returned otherwise.
*/
public function getMatchedWildcards(string $topicName): array
{
if (!preg_match($this->regexifiedTopicFilter, $topicName, $matches)) {
return [];
}
return array_slice($matches, 1);
}
/**
* Returns the callback for this subscription.
*/
public function getCallback(): ?\Closure
{
return $this->callback;
}
/**
* Returns the requested quality of service level.
*/
public function getQualityOfServiceLevel(): int
{
return $this->qualityOfService;
}
/**
* Sets the actual quality of service level.
*/
public function setQualityOfServiceLevel(int $qualityOfService): void
{
$this->qualityOfService = $qualityOfService;
}
}
-38
View File
@@ -1,38 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client;
/**
* Represents an unsubscribe request.
*
* @package PhpMqtt\Client
*/
class UnsubscribeRequest extends PendingMessage
{
/** @var string[] */
private array $topicFilters;
/**
* Creates a new unsubscribe request object.
*
* @param string[] $topicFilters
*/
public function __construct(int $messageId, array $topicFilters)
{
parent::__construct($messageId);
$this->topicFilters = array_values($topicFilters);
}
/**
* Returns the topic filters in this request.
*
* @return string[]
*/
public function getTopicFilters(): array
{
return $this->topicFilters;
}
}
@@ -1,51 +0,0 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
declare(strict_types=1);
namespace Tests\Feature;
use PhpMqtt\Client\Exceptions\ClientNotConnectedToBrokerException;
use PhpMqtt\Client\MqttClient;
use Tests\TestCase;
/**
* Tests that the client throws an exception if not connected.
*
* @package Tests\Feature
*/
class ActionsWithoutActiveConnectionTest extends TestCase
{
public function test_throws_exception_when_message_is_published_without_connecting_to_broker(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-not-connected');
$this->expectException(ClientNotConnectedToBrokerException::class);
$client->publish('foo/bar', 'baz');
}
public function test_throws_exception_when_topic_is_subscribed_without_connecting_to_broker(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-not-connected');
$this->expectException(ClientNotConnectedToBrokerException::class);
$client->subscribe('foo/bar', fn () => true);
}
public function test_throws_exception_when_topic_is_unsubscribed_without_connecting_to_broker(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-not-connected');
$this->expectException(ClientNotConnectedToBrokerException::class);
$client->unsubscribe('foo/bar');
}
public function test_throws_exception_when_disconnecting_without_connecting_to_broker_first(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-not-connected');
$this->expectException(ClientNotConnectedToBrokerException::class);
$client->disconnect();
}
}
@@ -1,93 +0,0 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
declare(strict_types=1);
namespace Tests\Feature;
use PhpMqtt\Client\MqttClient;
use Tests\TestCase;
/**
* Tests that the client utils (optional methods) work as intended.
*
* @package Tests\Feature
*/
class ClientUtilsTest extends TestCase
{
public function test_counts_sent_and_received_bytes_correctly(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-byte-count');
$client->connect(null, true);
// Even the connection request and acknowledgement have bytes.
$this->assertGreaterThan(0, $client->getSentBytes());
$this->assertGreaterThan(0, $client->getReceivedBytes());
// We therefore remember the current transfer stats and send some more data.
$sentBytesBeforePublish = $client->getSentBytes();
$receivedBytesBeforePublish = $client->getReceivedBytes();
$client->publish('foo/bar', 'baz-01', MqttClient::QOS_AT_MOST_ONCE);
$client->publish('foo/bar', 'baz-02', MqttClient::QOS_AT_LEAST_ONCE);
$client->publish('foo/bar', 'baz-03', MqttClient::QOS_EXACTLY_ONCE);
$this->assertGreaterThan($sentBytesBeforePublish, $client->getSentBytes());
$this->assertSame($receivedBytesBeforePublish, $client->getReceivedBytes());
// Also we receive all acknowledgements to update our transfer stats correctly.
$client->loop(true, true);
$this->assertGreaterThan($receivedBytesBeforePublish, $client->getReceivedBytes());
$client->disconnect();
}
public function test_is_connected_returns_correct_state(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-is-connected');
$client->connect(null, true);
$this->assertTrue($client->isConnected());
$client->disconnect();
$this->assertFalse($client->isConnected());
$client->connect(null, true);
$this->assertTrue($client->isConnected());
$client->disconnect();
$this->assertFalse($client->isConnected());
}
public function test_configured_client_id_is_returned_if_client_id_is_passed_to_constructor(): void
{
$clientId = 'test-configured-client-id';
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, $clientId);
$this->assertSame($clientId, $client->getClientId());
}
public function test_generated_client_id_is_returned_if_no_client_id_is_passed_to_constructor(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort);
$this->assertNotNull($client->getClientId());
$this->assertNotEmpty($client->getClientId());
}
public function test_configured_broker_host_and_port_are_returned(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort);
$this->assertSame($this->mqttBrokerHost, $client->getHost());
$this->assertSame($this->mqttBrokerPort, $client->getPort());
}
}
@@ -1,83 +0,0 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
declare(strict_types=1);
namespace Tests\Feature;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\MqttClient;
use Tests\TestCase;
/**
* Tests that the client is able to connect to a broker with custom connection settings.
*
* @package Tests\Feature
*/
class ConnectWithCustomConnectionSettingsTest extends TestCase
{
public function test_connecting_using_mqtt31_with_custom_connection_settings_works_as_intended(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPortWithAuthentication, 'test-custom-connection-settings', MqttClient::MQTT_3_1);
$connectionSettings = (new ConnectionSettings)
->setLastWillTopic('foo/last/will')
->setLastWillMessage('baz is out!')
->setLastWillQualityOfService(MqttClient::QOS_AT_MOST_ONCE)
->setRetainLastWill(true)
->setConnectTimeout(3)
->setSocketTimeout(3)
->setResendTimeout(3)
->setKeepAliveInterval(30)
->setUsername($this->mqttBrokerUsername)
->setPassword($this->mqttBrokerPassword)
->setUseTls(false)
->setTlsCertificateAuthorityFile(null)
->setTlsCertificateAuthorityPath(null)
->setTlsClientCertificateFile(null)
->setTlsClientCertificateKeyFile(null)
->setTlsClientCertificateKeyPassphrase(null)
->setTlsVerifyPeer(false)
->setTlsVerifyPeerName(false)
->setTlsSelfSignedAllowed(true);
$client->connect($connectionSettings);
$this->assertTrue($client->isConnected());
$client->disconnect();
}
public function test_connecting_using_mqtt311_with_custom_connection_settings_works_as_intended(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPortWithAuthentication, 'test-custom-connection-settings', MqttClient::MQTT_3_1_1);
$connectionSettings = (new ConnectionSettings)
->setLastWillTopic('foo/last/will')
->setLastWillMessage('baz is out!')
->setLastWillQualityOfService(MqttClient::QOS_AT_MOST_ONCE)
->setRetainLastWill(true)
->setConnectTimeout(3)
->setSocketTimeout(3)
->setResendTimeout(3)
->setKeepAliveInterval(30)
->setUsername($this->mqttBrokerUsername)
->setPassword($this->mqttBrokerPassword)
->setUseTls(false)
->setTlsCertificateAuthorityFile(null)
->setTlsCertificateAuthorityPath(null)
->setTlsClientCertificateFile(null)
->setTlsClientCertificateKeyFile(null)
->setTlsClientCertificateKeyPassphrase(null)
->setTlsVerifyPeer(false)
->setTlsVerifyPeerName(false)
->setTlsSelfSignedAllowed(true);
$client->connect($connectionSettings);
$this->assertTrue($client->isConnected());
$client->disconnect();
}
}
@@ -1,200 +0,0 @@
<?php
/** @noinspection PhpDocSignatureInspection */
/** @noinspection PhpUnhandledExceptionInspection */
declare(strict_types=1);
namespace Tests\Feature;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\Exceptions\ConfigurationInvalidException;
use PhpMqtt\Client\Exceptions\ConnectingToBrokerFailedException;
use PhpMqtt\Client\Exceptions\ProtocolNotSupportedException;
use PhpMqtt\Client\MqttClient;
use Tests\TestCase;
/**
* Tests that the client cannot connect with invalid configuration.
*
* @package Tests\Feature
*/
class ConnectWithInvalidConfigurationTest extends TestCase
{
public function invalidTimeouts(): array
{
return [
[0],
[-1],
[-100],
];
}
/**
* @dataProvider invalidTimeouts
*/
public function test_connect_timeout_cannot_be_below_1_second(int $timeout): void
{
$connectionSettings = (new ConnectionSettings)->setConnectTimeout($timeout);
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
/**
* @dataProvider invalidTimeouts
*/
public function test_socket_timeout_cannot_be_below_1_second(int $timeout): void
{
$connectionSettings = (new ConnectionSettings)->setSocketTimeout($timeout);
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
/**
* @dataProvider invalidTimeouts
*/
public function test_resend_timeout_cannot_be_below_1_second(int $timeout): void
{
$connectionSettings = (new ConnectionSettings)->setResendTimeout($timeout);
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
public function invalidKeepAliveIntervals(): array
{
return [
[0],
[-1],
[-100],
[65536],
[100000],
];
}
/**
* @dataProvider invalidKeepAliveIntervals
*/
public function test_keep_alive_interval_cannot_be_value_below_1_or_greater_than_65535(int $keepAliveInterval): void
{
$connectionSettings = (new ConnectionSettings)->setKeepAliveInterval($keepAliveInterval);
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
public function invalidUsernames(): array
{
return [
[''],
[' '],
[' '],
[' '],
];
}
/**
* @dataProvider invalidUsernames
*/
public function test_username_cannot_be_empty_or_whitespace(string $username): void
{
$connectionSettings = (new ConnectionSettings)->setUsername($username);
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
public function invalidLastWillTopics(): array
{
return [
[''],
[' '],
[' '],
[' '],
];
}
/**
* @dataProvider invalidLastWillTopics
*/
public function test_last_will_topic_cannot_be_empty_or_whitespace(string $topic): void
{
$connectionSettings = (new ConnectionSettings)->setLastWillTopic($topic);
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
public function invalidLastWillQualityOfService(): array
{
return [
[-1],
[3],
];
}
/**
* @dataProvider invalidLastWillQualityOfService
*/
public function test_last_will_quality_of_service_cannot_be_outside_the_0_to_2_range(int $qualityOfService): void
{
$connectionSettings = (new ConnectionSettings)->setLastWillQualityOfService($qualityOfService);
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
public function test_tls_certificate_authority_file_cannot_be_invalid_file_path(): void
{
$connectionSettings = (new ConnectionSettings)->setTlsCertificateAuthorityFile(__DIR__.'/not_existing_file');
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
public function test_tls_certificate_authority_path_cannot_be_invalid_directory_path(): void
{
$connectionSettings = (new ConnectionSettings)->setTlsCertificateAuthorityPath(__DIR__.'/not_existing_directory');
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
public function test_tls_client_certificate_file_cannot_be_invalid_file_path(): void
{
$connectionSettings = (new ConnectionSettings)->setTlsClientCertificateFile(__DIR__.'/not_existing_file');
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
public function test_tls_client_certificate_key_file_cannot_be_invalid_file_path(): void
{
$connectionSettings = (new ConnectionSettings)->setTlsClientCertificateKeyFile(__DIR__.'/not_existing_file');
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
public function test_tls_client_certificate_file_must_be_set_if_client_certificate_key_file_is_set(): void
{
$connectionSettings = (new ConnectionSettings)->setTlsClientCertificateKeyFile(__DIR__.'/../resources/invalid-test-certificate.key');
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
public function test_tls_client_certificate_key_file_must_be_set_if_client_certificate_key_passphrase_is_set(): void
{
$connectionSettings = (new ConnectionSettings)
->setTlsClientCertificateFile(__DIR__.'/../resources/invalid-test-certificate.crt')
->setTlsClientCertificateKeyPassphrase('some');
$this->connectAndExpectConfigurationExceptionUsingSettings($connectionSettings);
}
/**
* Performs the actual connection test using the given connection settings. Expects the settings to be invalid.
*
* @throws ConfigurationInvalidException
* @throws ConnectingToBrokerFailedException
* @throws ProtocolNotSupportedException
*/
private function connectAndExpectConfigurationExceptionUsingSettings(ConnectionSettings $connectionSettings): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-invalid-connection-settings');
$this->expectException(ConfigurationInvalidException::class);
$client->connect($connectionSettings);
}
}
@@ -1,36 +0,0 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
declare(strict_types=1);
namespace Tests\Feature;
use PhpMqtt\Client\Exceptions\ConnectingToBrokerFailedException;
use PhpMqtt\Client\MqttClient;
use Tests\TestCase;
/**
* Tests that the client throws an exception if connecting using invalid host and port.
*
* @package Tests\Feature
*/
class ConnectWithInvalidHostAndPortTest extends TestCase
{
public function test_throws_exception_when_connecting_using_invalid_host_and_port(): void
{
$client = new MqttClient('127.0.0.1', 56565, 'test-invalid-host');
$this->expectException(ConnectingToBrokerFailedException::class);
$this->expectExceptionCode(ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_SOCKET_ERROR);
try {
$client->connect(null, true);
} catch (ConnectingToBrokerFailedException $e) {
$this->assertGreaterThan(0, $e->getConnectionErrorCode());
$this->assertNotEmpty($e->getConnectionErrorMessage());
throw $e;
}
}
}
@@ -1,136 +0,0 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
declare(strict_types=1);
namespace Tests\Feature;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\Exceptions\ConnectingToBrokerFailedException;
use PhpMqtt\Client\MqttClient;
use Tests\TestCase;
/**
* Tests that the client is able to connect to a broker using TLS.
*
* @package Tests\Feature
*/
class ConnectWithTlsSettingsTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
if ($this->skipTlsTests) {
$this->markTestSkipped('TLS tests are disabled.');
}
}
public function test_connecting_with_tls_but_without_further_configuration_throws_for_self_signed_certificate(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsPort, 'test-tls-settings');
$connectionSettings = (new ConnectionSettings)
->setUseTls(true);
$this->expectException(ConnectingToBrokerFailedException::class);
$this->expectExceptionCode(ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_TLS_ERROR);
$client->connect($connectionSettings, true);
}
public function test_connecting_with_tls_with_ignored_self_signed_certificate_works_as_intended(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsPort, 'test-tls-settings');
$connectionSettings = (new ConnectionSettings)
->setUseTls(true)
->setTlsSelfSignedAllowed(true)
->setTlsVerifyPeer(false)
->setTlsVerifyPeerName(false);
$client->connect($connectionSettings, true);
$this->assertTrue($client->isConnected());
$client->disconnect();
}
public function test_connecting_with_tls_with_validated_self_signed_certificate_using_cafile__works_as_intended(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsPort, 'test-tls-settings');
$connectionSettings = (new ConnectionSettings)
->setUseTls(true)
->setTlsSelfSignedAllowed(false)
->setTlsVerifyPeer(true)
->setTlsVerifyPeerName(true)
->setTlsCertificateAuthorityFile($this->tlsCertificateDirectory . '/ca.crt');
$client->connect($connectionSettings, true);
$this->assertTrue($client->isConnected());
$client->disconnect();
}
public function test_connecting_with_tls_with_validated_self_signed_certificate_using_capath_works_as_intended(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsPort, 'test-tls-settings');
$connectionSettings = (new ConnectionSettings)
->setUseTls(true)
->setTlsSelfSignedAllowed(false)
->setTlsVerifyPeer(true)
->setTlsVerifyPeerName(true)
->setTlsCertificateAuthorityPath($this->tlsCertificateDirectory);
$client->connect($connectionSettings, true);
$this->assertTrue($client->isConnected());
$client->disconnect();
}
public function test_connecting_with_tls_and_client_certificate_with_validated_self_signed_certificate_works_as_intended(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsWithClientCertificatePort, 'test-tls-settings');
$connectionSettings = (new ConnectionSettings)
->setUseTls(true)
->setTlsSelfSignedAllowed(false)
->setTlsVerifyPeer(true)
->setTlsVerifyPeerName(true)
->setTlsCertificateAuthorityFile($this->tlsCertificateDirectory . '/ca.crt')
->setTlsClientCertificateFile($this->tlsCertificateDirectory . '/client.crt')
->setTlsClientCertificateKeyFile($this->tlsCertificateDirectory . '/client.key');
$client->connect($connectionSettings, true);
$this->assertTrue($client->isConnected());
$client->disconnect();
}
public function test_connecting_with_tls_and_passphrase_protected_client_certificate_with_validated_self_signed_certificate_works_as_intended(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerTlsWithClientCertificatePort, 'test-tls-settings');
$connectionSettings = (new ConnectionSettings)
->setUseTls(true)
->setTlsSelfSignedAllowed(false)
->setTlsVerifyPeer(true)
->setTlsVerifyPeerName(true)
->setTlsCertificateAuthorityFile($this->tlsCertificateDirectory . '/ca.crt')
->setTlsClientCertificateFile($this->tlsCertificateDirectory . '/client2.crt')
->setTlsClientCertificateKeyFile($this->tlsCertificateDirectory . '/client2.key')
->setTlsClientCertificateKeyPassphrase('s3cr3t');
$client->connect($connectionSettings, true);
$this->assertTrue($client->isConnected());
$client->disconnect();
}
}
@@ -1,116 +0,0 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
declare(strict_types=1);
namespace Tests\Feature;
use PhpMqtt\Client\MqttClient;
use Tests\TestCase;
/**
* Tests that the connected event handler work as intended.
*
* @package Tests\Feature
*/
class ConnectedEventHandlerTest extends TestCase
{
public function test_connected_event_handlers_are_called_every_time_the_client_connects_successfully(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-connected-event-handler');
$handlerCallCount = 0;
$handler = function () use (&$handlerCallCount) {
$handlerCallCount++;
};
$client->registerConnectedEventHandler($handler);
$client->connect();
$this->assertSame(1, $handlerCallCount);
$client->disconnect();
$client->connect();
$this->assertSame(2, $handlerCallCount);
$client->disconnect();
$client->connect();
$this->assertSame(3, $handlerCallCount);
$client->disconnect();
}
public function test_connected_event_handlers_can_be_unregistered_and_will_not_be_called_anymore(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-connected-event-handler');
$handlerCallCount = 0;
$handler = function () use (&$handlerCallCount) {
$handlerCallCount++;
};
$client->registerConnectedEventHandler($handler);
$client->connect();
$this->assertSame(1, $handlerCallCount);
$client->unregisterConnectedEventHandler($handler);
$client->disconnect();
$client->connect();
$this->assertSame(1, $handlerCallCount);
$client->registerConnectedEventHandler($handler);
$client->disconnect();
$client->connect();
$this->assertSame(2, $handlerCallCount);
$client->unregisterConnectedEventHandler($handler);
$client->disconnect();
$client->connect();
$this->assertSame(2, $handlerCallCount);
$client->disconnect();
}
public function test_connected_event_handlers_can_throw_exceptions_which_does_not_affect_other_handlers_or_the_application(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-connected-event-handler');
$handlerCallCount = 0;
$handler1 = function () use (&$handlerCallCount) {
$handlerCallCount++;
};
$handler2 = function () {
throw new \Exception('Something went wrong!');
};
$client->registerConnectedEventHandler($handler1);
$client->registerConnectedEventHandler($handler2);
$client->connect();
$this->assertSame(1, $handlerCallCount);
$client->disconnect();
}
public function test_connected_event_handler_is_passed_the_mqtt_client_and_the_auto_reconnect_flag_as_arguments(): void
{
$client = new MqttClient($this->mqttBrokerHost, $this->mqttBrokerPort, 'test-connected-event-handler');
$client->registerConnectedEventHandler(function ($mqttClient, $isAutoReconnect) {
$this->assertInstanceOf(MqttClient::class, $mqttClient);
$this->assertIsBool($isAutoReconnect);
$this->assertFalse($isAutoReconnect);
});
$client->connect();
$client->disconnect();
}
}

Some files were not shown because too many files have changed in this diff Show More