🐞 fix: Error cleanup, add test harness
This commit is contained in:
+20
-1
@@ -1,5 +1,24 @@
|
|||||||
{
|
{
|
||||||
|
"name": "keith/ironkanban",
|
||||||
|
"type": "project",
|
||||||
"require": {
|
"require": {
|
||||||
"symfony/yaml": "^7.4"
|
"php": "^8.2",
|
||||||
|
"symfony/yaml": "^7.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^11.0"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"IronKanban\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"IronKanban\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "phpunit"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1787
-3
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,7 @@ class Task {
|
|||||||
* @param string $priority The priority level of the task
|
* @param string $priority The priority level of the task
|
||||||
* @param bool $isActive Whether the task is active
|
* @param bool $isActive Whether the task is active
|
||||||
* @param string $body The task description
|
* @param string $body The task description
|
||||||
|
* @param string $fileName The task file name
|
||||||
* @param array $meta Additional metadata (optional)
|
* @param array $meta Additional metadata (optional)
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -52,6 +53,7 @@ class Task {
|
|||||||
public string $priority,
|
public string $priority,
|
||||||
public bool $isActive,
|
public bool $isActive,
|
||||||
public string $body,
|
public string $body,
|
||||||
|
public string $fileName,
|
||||||
public array $meta = []
|
public array $meta = []
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ declare(strict_types=1);
|
|||||||
namespace IronKanban\Repository;
|
namespace IronKanban\Repository;
|
||||||
|
|
||||||
use IronKanban\Domain\Task;
|
use IronKanban\Domain\Task;
|
||||||
use IronKanban\Markdown\FrontMatterDocument;
|
|
||||||
use IronKanban\Markdown\FrontMatterParser;
|
use IronKanban\Markdown\FrontMatterParser;
|
||||||
use IronKanban\Support\FileLock;
|
use IronKanban\Support\FileLock;
|
||||||
|
use IronKanban\Support\IdGenerator;
|
||||||
use IronKanban\Support\Paths;
|
use IronKanban\Support\Paths;
|
||||||
|
use IronKanban\Support\TaskMapper;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,10 +41,14 @@ class TaskRepository {
|
|||||||
*
|
*
|
||||||
* @param Paths $paths The paths helper
|
* @param Paths $paths The paths helper
|
||||||
* @param FrontMatterParser $frontMatterParser The front matter parser
|
* @param FrontMatterParser $frontMatterParser The front matter parser
|
||||||
|
* @param TaskMapper $taskMapper The task mapper
|
||||||
|
* @param IdGenerator $idGenerator The ID generator
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Paths $paths,
|
private readonly Paths $paths,
|
||||||
private readonly FrontMatterParser $frontMatterParser
|
private readonly FrontMatterParser $frontMatterParser,
|
||||||
|
private readonly TaskMapper $taskMapper,
|
||||||
|
private readonly IdGenerator $idGenerator
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +74,14 @@ class TaskRepository {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$tasks[] = $this->_mapFileToTask($projectId, $filePath);
|
$fileName = basename($filePath);
|
||||||
|
$document = $this->frontMatterParser->parseFile($filePath);
|
||||||
|
|
||||||
|
$tasks[] = $this->taskMapper->fromDocument(
|
||||||
|
$projectId,
|
||||||
|
$fileName,
|
||||||
|
$document
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
usort(
|
usort(
|
||||||
@@ -96,13 +108,13 @@ class TaskRepository {
|
|||||||
* @throws RuntimeException If task not found
|
* @throws RuntimeException If task not found
|
||||||
*/
|
*/
|
||||||
public function get(string $projectId, string $taskId): Task {
|
public function get(string $projectId, string $taskId): Task {
|
||||||
$filePath = $this->paths->getTaskFilePath($projectId, $taskId);
|
foreach ($this->getAll($projectId) as $task) {
|
||||||
|
if ($task->id === $taskId) {
|
||||||
if (!is_file($filePath)) {
|
return $task;
|
||||||
throw new RuntimeException("Task not found: {$taskId}");
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->_mapFileToTask($projectId, $filePath);
|
throw new RuntimeException("Task not found: {$taskId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -115,12 +127,13 @@ class TaskRepository {
|
|||||||
public function save(Task $task): void {
|
public function save(Task $task): void {
|
||||||
$this->paths->ensureTasksPath($task->projectId);
|
$this->paths->ensureTasksPath($task->projectId);
|
||||||
|
|
||||||
$filePath = $this->paths->getTaskFilePath($task->projectId, $task->id);
|
$filePath = $this->paths->getTaskFilePath($task->projectId, $task->fileName);
|
||||||
$lockPath = $filePath . '.lock';
|
$lockPath = $filePath . '.lock';
|
||||||
|
|
||||||
FileLock::run(
|
FileLock::run(
|
||||||
$lockPath, function () use ($task, $filePath): void {
|
$lockPath,
|
||||||
$document = $this->_mapTaskToDocument($task);
|
function () use ($task, $filePath): void {
|
||||||
|
$document = $this->taskMapper->toDocument($task);
|
||||||
$contents = $this->frontMatterParser->dump($document);
|
$contents = $this->frontMatterParser->dump($document);
|
||||||
|
|
||||||
$this->_atomicWrite($filePath, $contents);
|
$this->_atomicWrite($filePath, $contents);
|
||||||
@@ -141,14 +154,17 @@ class TaskRepository {
|
|||||||
$projectId = $this->_requireString($data, 'projectId');
|
$projectId = $this->_requireString($data, 'projectId');
|
||||||
$title = $this->_requireString($data, 'title');
|
$title = $this->_requireString($data, 'title');
|
||||||
$body = (string) ($data['body'] ?? '');
|
$body = (string) ($data['body'] ?? '');
|
||||||
$column = $this->_normalizeColumn((string) ($data['column'] ?? 'backlog'));
|
$column = $this->taskMapper->normalizeColumn(
|
||||||
|
(string) ($data['column'] ?? 'backlog')
|
||||||
|
);
|
||||||
$priority = (string) ($data['priority'] ?? 'normal');
|
$priority = (string) ($data['priority'] ?? 'normal');
|
||||||
$completed = (bool) ($data['completed'] ?? false);
|
$completed = (bool) ($data['completed'] ?? false);
|
||||||
$isActive = (bool) ($data['isActive'] ?? true);
|
$isActive = (bool) ($data['isActive'] ?? true);
|
||||||
$order = isset($data['order']) ? (int) $data['order'] : 100;
|
$order = isset($data['order']) ? (int) $data['order'] : 100;
|
||||||
$now = gmdate('c');
|
$now = gmdate('c');
|
||||||
|
|
||||||
$taskId = $this->_generateTaskId();
|
$taskId = $this->idGenerator->generateTaskId();
|
||||||
|
$fileName = $this->idGenerator->generateTaskFileName($taskId);
|
||||||
|
|
||||||
$meta = [
|
$meta = [
|
||||||
'id' => $taskId,
|
'id' => $taskId,
|
||||||
@@ -165,12 +181,16 @@ class TaskRepository {
|
|||||||
'tags' => [],
|
'tags' => [],
|
||||||
];
|
];
|
||||||
|
|
||||||
if (isset($data['status'])) {
|
if (array_key_exists('status', $data)) {
|
||||||
$meta['status'] = (string) $data['status'];
|
$meta['status'] = $data['status'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($data['statusNote'])) {
|
if (array_key_exists('statusNote', $data)) {
|
||||||
$meta['status_note'] = (string) $data['statusNote'];
|
$meta['status_note'] = $data['statusNote'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('section', $data)) {
|
||||||
|
$meta['section'] = $data['section'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$task = new Task(
|
$task = new Task(
|
||||||
@@ -183,6 +203,7 @@ class TaskRepository {
|
|||||||
priority: $priority,
|
priority: $priority,
|
||||||
isActive: $isActive,
|
isActive: $isActive,
|
||||||
body: $body,
|
body: $body,
|
||||||
|
fileName: $fileName,
|
||||||
meta: $meta
|
meta: $meta
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -202,15 +223,17 @@ class TaskRepository {
|
|||||||
* @throws RuntimeException If task not found
|
* @throws RuntimeException If task not found
|
||||||
*/
|
*/
|
||||||
public function delete(string $projectId, string $taskId): void {
|
public function delete(string $projectId, string $taskId): void {
|
||||||
$filePath = $this->paths->getTaskFilePath($projectId, $taskId);
|
$task = $this->get($projectId, $taskId);
|
||||||
|
$filePath = $this->paths->getTaskFilePath($projectId, $task->fileName);
|
||||||
$lockPath = $filePath . '.lock';
|
$lockPath = $filePath . '.lock';
|
||||||
|
|
||||||
|
FileLock::run(
|
||||||
|
$lockPath,
|
||||||
|
function () use ($filePath): void {
|
||||||
if (!is_file($filePath)) {
|
if (!is_file($filePath)) {
|
||||||
throw new RuntimeException("Task not found: {$taskId}");
|
throw new RuntimeException("Task file not found: {$filePath}");
|
||||||
}
|
}
|
||||||
|
|
||||||
FileLock::run(
|
|
||||||
$lockPath, function () use ($filePath): void {
|
|
||||||
if (!unlink($filePath)) {
|
if (!unlink($filePath)) {
|
||||||
throw new RuntimeException(
|
throw new RuntimeException(
|
||||||
"Unable to delete task file: {$filePath}"
|
"Unable to delete task file: {$filePath}"
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID Generator
|
||||||
|
*
|
||||||
|
* PHP version 8.2+
|
||||||
|
*
|
||||||
|
* Generates unique identifiers for tasks and related files.
|
||||||
|
*
|
||||||
|
* @category Support
|
||||||
|
* @package IronKanban\Support
|
||||||
|
* @author Keith Solomon <keith@keithsolomon.net>
|
||||||
|
* @license Unlicense https://unlicense.org
|
||||||
|
* @link https://git.keithsolomon.net/keith/IronKanban
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace IronKanban\Support;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IdGenerator generates unique identifiers
|
||||||
|
*
|
||||||
|
* @category Support
|
||||||
|
* @package IronKanban\Support
|
||||||
|
* @author Keith Solomon <keith@keithsolomon.net>
|
||||||
|
* @license Unlicense https://unlicense.org
|
||||||
|
* @link https://git.keithsolomon.net/keith/IronKanban
|
||||||
|
*/
|
||||||
|
class IdGenerator {
|
||||||
|
/**
|
||||||
|
* Generate a unique task ID
|
||||||
|
*
|
||||||
|
* @return string The generated task ID
|
||||||
|
*/
|
||||||
|
public function generateTaskId(): string {
|
||||||
|
return 'task-' . gmdate('Ymd-His') . '-' .
|
||||||
|
substr(bin2hex(random_bytes(3)), 0, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a task file name
|
||||||
|
*
|
||||||
|
* @param string $taskId The task identifier
|
||||||
|
*
|
||||||
|
* @return string The generated file name
|
||||||
|
*/
|
||||||
|
public function generateTaskFileName(string $taskId): string {
|
||||||
|
return $taskId . '.md';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ class Paths {
|
|||||||
*
|
*
|
||||||
* @throws RuntimeException If the project root is invalid
|
* @throws RuntimeException If the project root is invalid
|
||||||
*/
|
*/
|
||||||
public function __construct( private readonly string $projectRoot ) {
|
public function __construct( private string $projectRoot ) {
|
||||||
$realRoot = realpath($projectRoot);
|
$realRoot = realpath($projectRoot);
|
||||||
|
|
||||||
if ($realRoot === false || !is_dir($realRoot)) {
|
if ($realRoot === false || !is_dir($realRoot)) {
|
||||||
@@ -52,8 +52,7 @@ class Paths {
|
|||||||
*
|
*
|
||||||
* @return string The project root path
|
* @return string The project root path
|
||||||
*/
|
*/
|
||||||
public function getProjectRoot(): string
|
public function getProjectRoot(): string {
|
||||||
{
|
|
||||||
return $this->projectRoot;
|
return $this->projectRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Task Mapper
|
||||||
|
*
|
||||||
|
* PHP version 8.2+
|
||||||
|
*
|
||||||
|
* Maps between Task domain objects and FrontMatterDocument representations.
|
||||||
|
*
|
||||||
|
* @category Support
|
||||||
|
* @package IronKanban\Support
|
||||||
|
* @author Keith Solomon <keith@keithsolomon.net>
|
||||||
|
* @license Unlicense https://unlicense.org
|
||||||
|
* @link https://git.keithsolomon.net/keith/IronKanban
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace IronKanban\Support;
|
||||||
|
|
||||||
|
use IronKanban\Domain\Task;
|
||||||
|
use IronKanban\Markdown\FrontMatterDocument;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TaskMapper maps Task objects and FrontMatterDocument representations
|
||||||
|
*
|
||||||
|
* @category Support
|
||||||
|
* @package IronKanban\Support
|
||||||
|
* @author Keith Solomon <keith@keithsolomon.net>
|
||||||
|
* @license Unlicense https://unlicense.org
|
||||||
|
* @link https://git.keithsolomon.net/keith/IronKanban
|
||||||
|
*/
|
||||||
|
class TaskMapper {
|
||||||
|
/**
|
||||||
|
* Convert a FrontMatterDocument to a Task domain object
|
||||||
|
*
|
||||||
|
* @param string $projectId The project identifier
|
||||||
|
* @param string $fileName The task file name
|
||||||
|
* @param FrontMatterDocument $document The front matter document
|
||||||
|
*
|
||||||
|
* @return Task The mapped task
|
||||||
|
*/
|
||||||
|
public function fromDocument(
|
||||||
|
string $projectId,
|
||||||
|
string $fileName,
|
||||||
|
FrontMatterDocument $document
|
||||||
|
): Task {
|
||||||
|
$meta = $document->meta;
|
||||||
|
|
||||||
|
$id = (string) ($meta['id'] ?? pathinfo($fileName, PATHINFO_FILENAME));
|
||||||
|
$title = (string) ($meta['title'] ?? $id);
|
||||||
|
|
||||||
|
$column = isset($meta['column'])
|
||||||
|
? $this->normalizeColumn((string) $meta['column'])
|
||||||
|
: $this->mapLegacySectionToColumn(
|
||||||
|
(string) ($meta['section'] ?? 'backlog')
|
||||||
|
);
|
||||||
|
|
||||||
|
$order = isset($meta['order']) ? (int) $meta['order'] : 100;
|
||||||
|
$completed = (bool) ($meta['completed'] ?? false);
|
||||||
|
$priority = (string) ($meta['priority'] ?? 'normal');
|
||||||
|
$isActive = (bool) ($meta['is_active'] ?? true);
|
||||||
|
|
||||||
|
return new Task(
|
||||||
|
id: $id,
|
||||||
|
title: $title,
|
||||||
|
projectId: (string) ($meta['project_id'] ?? $projectId),
|
||||||
|
column: $column,
|
||||||
|
order: $order,
|
||||||
|
completed: $completed,
|
||||||
|
priority: $priority,
|
||||||
|
isActive: $isActive,
|
||||||
|
body: $document->body,
|
||||||
|
fileName: $fileName,
|
||||||
|
meta: $meta
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a Task domain object to a FrontMatterDocument
|
||||||
|
*
|
||||||
|
* @param Task $task The task to convert
|
||||||
|
*
|
||||||
|
* @return FrontMatterDocument The mapped document
|
||||||
|
*/
|
||||||
|
public function toDocument(Task $task): FrontMatterDocument {
|
||||||
|
$meta = $task->meta;
|
||||||
|
|
||||||
|
$meta['id'] = $task->id;
|
||||||
|
$meta['type'] = 'task';
|
||||||
|
$meta['title'] = $task->title;
|
||||||
|
$meta['project_id'] = $task->projectId;
|
||||||
|
$meta['column'] = $task->column;
|
||||||
|
$meta['order'] = $task->order;
|
||||||
|
$meta['completed'] = $task->completed;
|
||||||
|
$meta['priority'] = $task->priority;
|
||||||
|
$meta['is_active'] = $task->isActive;
|
||||||
|
|
||||||
|
if (!isset($meta['created']) || trim((string) $meta['created']) === '') {
|
||||||
|
$meta['created'] = gmdate('c');
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta['updated'] = gmdate('c');
|
||||||
|
|
||||||
|
return new FrontMatterDocument($meta, $task->body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a column name
|
||||||
|
*
|
||||||
|
* @param string $column The column name to normalize
|
||||||
|
*
|
||||||
|
* @return string The normalized column name
|
||||||
|
*/
|
||||||
|
public function normalizeColumn(string $column): string {
|
||||||
|
$column = trim(strtolower($column));
|
||||||
|
|
||||||
|
if ($column === '') {
|
||||||
|
return 'backlog';
|
||||||
|
}
|
||||||
|
|
||||||
|
$column = preg_replace('/[^a-z0-9]+/', '-', $column) ?? 'backlog';
|
||||||
|
$column = trim($column, '-');
|
||||||
|
|
||||||
|
return $column !== '' ? $column : 'backlog';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a legacy section name to a modern column name
|
||||||
|
*
|
||||||
|
* @param string $section The legacy section name
|
||||||
|
*
|
||||||
|
* @return string The normalized column name
|
||||||
|
*/
|
||||||
|
public function mapLegacySectionToColumn(string $section): string {
|
||||||
|
return $this->normalizeColumn($section);
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+1
-1
@@ -19,4 +19,4 @@ if (PHP_VERSION_ID < 50600) {
|
|||||||
|
|
||||||
require_once __DIR__ . '/composer/autoload_real.php';
|
require_once __DIR__ . '/composer/autoload_real.php';
|
||||||
|
|
||||||
return ComposerAutoloaderInit1d14b066a73960476c178ecf92bed18f::getLoader();
|
return ComposerAutoloaderInitf25b5b4c093b1a5112f6e1a1bba70ad2::getLoader();
|
||||||
|
|||||||
Vendored
+119
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy PHP file generated by Composer
|
||||||
|
*
|
||||||
|
* This file includes the referenced bin path (../nikic/php-parser/bin/php-parse)
|
||||||
|
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||||
|
*
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer;
|
||||||
|
|
||||||
|
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||||
|
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 80000) {
|
||||||
|
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BinProxyWrapper
|
||||||
|
{
|
||||||
|
private $handle;
|
||||||
|
private $position;
|
||||||
|
private $realpath;
|
||||||
|
|
||||||
|
public function stream_open($path, $mode, $options, &$opened_path)
|
||||||
|
{
|
||||||
|
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||||
|
$opened_path = substr($path, 17);
|
||||||
|
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||||
|
$opened_path = $this->realpath;
|
||||||
|
$this->handle = fopen($this->realpath, $mode);
|
||||||
|
$this->position = 0;
|
||||||
|
|
||||||
|
return (bool) $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_read($count)
|
||||||
|
{
|
||||||
|
$data = fread($this->handle, $count);
|
||||||
|
|
||||||
|
if ($this->position === 0) {
|
||||||
|
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->position += strlen($data);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_cast($castAs)
|
||||||
|
{
|
||||||
|
return $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_close()
|
||||||
|
{
|
||||||
|
fclose($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_lock($operation)
|
||||||
|
{
|
||||||
|
return $operation ? flock($this->handle, $operation) : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_seek($offset, $whence)
|
||||||
|
{
|
||||||
|
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||||
|
$this->position = ftell($this->handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_tell()
|
||||||
|
{
|
||||||
|
return $this->position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_eof()
|
||||||
|
{
|
||||||
|
return feof($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_stat()
|
||||||
|
{
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_set_option($option, $arg1, $arg2)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url_stat($path, $flags)
|
||||||
|
{
|
||||||
|
$path = substr($path, 17);
|
||||||
|
if (file_exists($path)) {
|
||||||
|
return stat($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||||
|
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||||
|
) {
|
||||||
|
return include("phpvfscomposer://" . __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return include __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse';
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
setlocal DISABLEDELAYEDEXPANSION
|
||||||
|
SET BIN_TARGET=%~dp0/php-parse
|
||||||
|
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||||
|
php "%BIN_TARGET%" %*
|
||||||
Vendored
+122
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy PHP file generated by Composer
|
||||||
|
*
|
||||||
|
* This file includes the referenced bin path (../phpunit/phpunit/phpunit)
|
||||||
|
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||||
|
*
|
||||||
|
* @generated
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer;
|
||||||
|
|
||||||
|
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||||
|
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||||
|
$GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST'] = $GLOBALS['__PHPUNIT_ISOLATION_BLACKLIST'] = array(realpath(__DIR__ . '/..'.'/phpunit/phpunit/phpunit'));
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 80000) {
|
||||||
|
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BinProxyWrapper
|
||||||
|
{
|
||||||
|
private $handle;
|
||||||
|
private $position;
|
||||||
|
private $realpath;
|
||||||
|
|
||||||
|
public function stream_open($path, $mode, $options, &$opened_path)
|
||||||
|
{
|
||||||
|
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||||
|
$opened_path = substr($path, 17);
|
||||||
|
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||||
|
$opened_path = 'phpvfscomposer://'.$this->realpath;
|
||||||
|
$this->handle = fopen($this->realpath, $mode);
|
||||||
|
$this->position = 0;
|
||||||
|
|
||||||
|
return (bool) $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_read($count)
|
||||||
|
{
|
||||||
|
$data = fread($this->handle, $count);
|
||||||
|
|
||||||
|
if ($this->position === 0) {
|
||||||
|
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||||
|
}
|
||||||
|
$data = str_replace('__DIR__', var_export(dirname($this->realpath), true), $data);
|
||||||
|
$data = str_replace('__FILE__', var_export($this->realpath, true), $data);
|
||||||
|
|
||||||
|
$this->position += strlen($data);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_cast($castAs)
|
||||||
|
{
|
||||||
|
return $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_close()
|
||||||
|
{
|
||||||
|
fclose($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_lock($operation)
|
||||||
|
{
|
||||||
|
return $operation ? flock($this->handle, $operation) : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_seek($offset, $whence)
|
||||||
|
{
|
||||||
|
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||||
|
$this->position = ftell($this->handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_tell()
|
||||||
|
{
|
||||||
|
return $this->position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_eof()
|
||||||
|
{
|
||||||
|
return feof($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_stat()
|
||||||
|
{
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream_set_option($option, $arg1, $arg2)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url_stat($path, $flags)
|
||||||
|
{
|
||||||
|
$path = substr($path, 17);
|
||||||
|
if (file_exists($path)) {
|
||||||
|
return stat($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||||
|
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||||
|
) {
|
||||||
|
return include("phpvfscomposer://" . __DIR__ . '/..'.'/phpunit/phpunit/phpunit');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return include __DIR__ . '/..'.'/phpunit/phpunit/phpunit';
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
setlocal DISABLEDELAYEDEXPANSION
|
||||||
|
SET BIN_TARGET=%~dp0/phpunit
|
||||||
|
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
|
||||||
|
php "%BIN_TARGET%" %*
|
||||||
+1225
File diff suppressed because it is too large
Load Diff
Vendored
+2
@@ -6,6 +6,8 @@ $vendorDir = dirname(__DIR__);
|
|||||||
$baseDir = dirname($vendorDir);
|
$baseDir = dirname($vendorDir);
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
|
'6124b4c8570aa390c21fafd04a26c69f' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
|
||||||
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
|
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
|
||||||
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
|
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
|
||||||
|
'ec07570ca5a812141189b1fa81503674' => $vendorDir . '/phpunit/phpunit/src/Framework/Assert/Functions.php',
|
||||||
);
|
);
|
||||||
|
|||||||
Vendored
+4
@@ -8,4 +8,8 @@ $baseDir = dirname($vendorDir);
|
|||||||
return array(
|
return array(
|
||||||
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
|
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
|
||||||
'Symfony\\Component\\Yaml\\' => array($vendorDir . '/symfony/yaml'),
|
'Symfony\\Component\\Yaml\\' => array($vendorDir . '/symfony/yaml'),
|
||||||
|
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
|
||||||
|
'IronKanban\\Tests\\' => array($baseDir . '/tests'),
|
||||||
|
'IronKanban\\' => array($baseDir . '/src'),
|
||||||
|
'DeepCopy\\' => array($vendorDir . '/myclabs/deep-copy/src/DeepCopy'),
|
||||||
);
|
);
|
||||||
|
|||||||
Vendored
+5
-5
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
// autoload_real.php @generated by Composer
|
// autoload_real.php @generated by Composer
|
||||||
|
|
||||||
class ComposerAutoloaderInit1d14b066a73960476c178ecf92bed18f
|
class ComposerAutoloaderInitf25b5b4c093b1a5112f6e1a1bba70ad2
|
||||||
{
|
{
|
||||||
private static $loader;
|
private static $loader;
|
||||||
|
|
||||||
@@ -24,16 +24,16 @@ class ComposerAutoloaderInit1d14b066a73960476c178ecf92bed18f
|
|||||||
|
|
||||||
require __DIR__ . '/platform_check.php';
|
require __DIR__ . '/platform_check.php';
|
||||||
|
|
||||||
spl_autoload_register(array('ComposerAutoloaderInit1d14b066a73960476c178ecf92bed18f', 'loadClassLoader'), true, true);
|
spl_autoload_register(array('ComposerAutoloaderInitf25b5b4c093b1a5112f6e1a1bba70ad2', 'loadClassLoader'), true, true);
|
||||||
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
|
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
|
||||||
spl_autoload_unregister(array('ComposerAutoloaderInit1d14b066a73960476c178ecf92bed18f', 'loadClassLoader'));
|
spl_autoload_unregister(array('ComposerAutoloaderInitf25b5b4c093b1a5112f6e1a1bba70ad2', 'loadClassLoader'));
|
||||||
|
|
||||||
require __DIR__ . '/autoload_static.php';
|
require __DIR__ . '/autoload_static.php';
|
||||||
call_user_func(\Composer\Autoload\ComposerStaticInit1d14b066a73960476c178ecf92bed18f::getInitializer($loader));
|
call_user_func(\Composer\Autoload\ComposerStaticInitf25b5b4c093b1a5112f6e1a1bba70ad2::getInitializer($loader));
|
||||||
|
|
||||||
$loader->register(true);
|
$loader->register(true);
|
||||||
|
|
||||||
$filesToLoad = \Composer\Autoload\ComposerStaticInit1d14b066a73960476c178ecf92bed18f::$files;
|
$filesToLoad = \Composer\Autoload\ComposerStaticInitf25b5b4c093b1a5112f6e1a1bba70ad2::$files;
|
||||||
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
|
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
|
||||||
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
|
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
|
||||||
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
|
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
|
||||||
|
|||||||
Vendored
+1260
-4
File diff suppressed because it is too large
Load Diff
Vendored
+1891
-1
File diff suppressed because it is too large
Load Diff
Vendored
+253
-10
@@ -1,24 +1,258 @@
|
|||||||
<?php return array(
|
<?php return array(
|
||||||
'root' => array(
|
'root' => array(
|
||||||
'name' => '__root__',
|
'name' => 'keith/ironkanban',
|
||||||
'pretty_version' => '1.0.0+no-version-set',
|
'pretty_version' => 'dev-main',
|
||||||
'version' => '1.0.0.0',
|
'version' => 'dev-main',
|
||||||
'reference' => null,
|
'reference' => 'adabacc48d6d86d9a514c83113405be126de9ae9',
|
||||||
'type' => 'library',
|
'type' => 'project',
|
||||||
'install_path' => __DIR__ . '/../../',
|
'install_path' => __DIR__ . '/../../',
|
||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
'dev' => true,
|
'dev' => true,
|
||||||
),
|
),
|
||||||
'versions' => array(
|
'versions' => array(
|
||||||
'__root__' => array(
|
'keith/ironkanban' => array(
|
||||||
'pretty_version' => '1.0.0+no-version-set',
|
'pretty_version' => 'dev-main',
|
||||||
'version' => '1.0.0.0',
|
'version' => 'dev-main',
|
||||||
'reference' => null,
|
'reference' => 'adabacc48d6d86d9a514c83113405be126de9ae9',
|
||||||
'type' => 'library',
|
'type' => 'project',
|
||||||
'install_path' => __DIR__ . '/../../',
|
'install_path' => __DIR__ . '/../../',
|
||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
'dev_requirement' => false,
|
'dev_requirement' => false,
|
||||||
),
|
),
|
||||||
|
'myclabs/deep-copy' => array(
|
||||||
|
'pretty_version' => '1.13.4',
|
||||||
|
'version' => '1.13.4.0',
|
||||||
|
'reference' => '07d290f0c47959fd5eed98c95ee5602db07e0b6a',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../myclabs/deep-copy',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'nikic/php-parser' => array(
|
||||||
|
'pretty_version' => 'v5.7.0',
|
||||||
|
'version' => '5.7.0.0',
|
||||||
|
'reference' => 'dca41cd15c2ac9d055ad70dbfd011130757d1f82',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../nikic/php-parser',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'phar-io/manifest' => array(
|
||||||
|
'pretty_version' => '2.0.4',
|
||||||
|
'version' => '2.0.4.0',
|
||||||
|
'reference' => '54750ef60c58e43759730615a392c31c80e23176',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../phar-io/manifest',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'phar-io/version' => array(
|
||||||
|
'pretty_version' => '3.2.1',
|
||||||
|
'version' => '3.2.1.0',
|
||||||
|
'reference' => '4f7fd7836c6f332bb2933569e566a0d6c4cbed74',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../phar-io/version',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'phpunit/php-code-coverage' => array(
|
||||||
|
'pretty_version' => '11.0.12',
|
||||||
|
'version' => '11.0.12.0',
|
||||||
|
'reference' => '2c1ed04922802c15e1de5d7447b4856de949cf56',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../phpunit/php-code-coverage',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'phpunit/php-file-iterator' => array(
|
||||||
|
'pretty_version' => '5.1.1',
|
||||||
|
'version' => '5.1.1.0',
|
||||||
|
'reference' => '2f3a64888c814fc235386b7387dd5b5ed92ad903',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../phpunit/php-file-iterator',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'phpunit/php-invoker' => array(
|
||||||
|
'pretty_version' => '5.0.1',
|
||||||
|
'version' => '5.0.1.0',
|
||||||
|
'reference' => 'c1ca3814734c07492b3d4c5f794f4b0995333da2',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../phpunit/php-invoker',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'phpunit/php-text-template' => array(
|
||||||
|
'pretty_version' => '4.0.1',
|
||||||
|
'version' => '4.0.1.0',
|
||||||
|
'reference' => '3e0404dc6b300e6bf56415467ebcb3fe4f33e964',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../phpunit/php-text-template',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'phpunit/php-timer' => array(
|
||||||
|
'pretty_version' => '7.0.1',
|
||||||
|
'version' => '7.0.1.0',
|
||||||
|
'reference' => '3b415def83fbcb41f991d9ebf16ae4ad8b7837b3',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../phpunit/php-timer',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'phpunit/phpunit' => array(
|
||||||
|
'pretty_version' => '11.5.55',
|
||||||
|
'version' => '11.5.55.0',
|
||||||
|
'reference' => 'adc7262fccc12de2b30f12a8aa0b33775d814f00',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../phpunit/phpunit',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'sebastian/cli-parser' => array(
|
||||||
|
'pretty_version' => '3.0.2',
|
||||||
|
'version' => '3.0.2.0',
|
||||||
|
'reference' => '15c5dd40dc4f38794d383bb95465193f5e0ae180',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../sebastian/cli-parser',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'sebastian/code-unit' => array(
|
||||||
|
'pretty_version' => '3.0.3',
|
||||||
|
'version' => '3.0.3.0',
|
||||||
|
'reference' => '54391c61e4af8078e5b276ab082b6d3c54c9ad64',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../sebastian/code-unit',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'sebastian/code-unit-reverse-lookup' => array(
|
||||||
|
'pretty_version' => '4.0.1',
|
||||||
|
'version' => '4.0.1.0',
|
||||||
|
'reference' => '183a9b2632194febd219bb9246eee421dad8d45e',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../sebastian/code-unit-reverse-lookup',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'sebastian/comparator' => array(
|
||||||
|
'pretty_version' => '6.3.3',
|
||||||
|
'version' => '6.3.3.0',
|
||||||
|
'reference' => '2c95e1e86cb8dd41beb8d502057d1081ccc8eca9',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../sebastian/comparator',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'sebastian/complexity' => array(
|
||||||
|
'pretty_version' => '4.0.1',
|
||||||
|
'version' => '4.0.1.0',
|
||||||
|
'reference' => 'ee41d384ab1906c68852636b6de493846e13e5a0',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../sebastian/complexity',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'sebastian/diff' => array(
|
||||||
|
'pretty_version' => '6.0.2',
|
||||||
|
'version' => '6.0.2.0',
|
||||||
|
'reference' => 'b4ccd857127db5d41a5b676f24b51371d76d8544',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../sebastian/diff',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'sebastian/environment' => array(
|
||||||
|
'pretty_version' => '7.2.1',
|
||||||
|
'version' => '7.2.1.0',
|
||||||
|
'reference' => 'a5c75038693ad2e8d4b6c15ba2403532647830c4',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../sebastian/environment',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'sebastian/exporter' => array(
|
||||||
|
'pretty_version' => '6.3.2',
|
||||||
|
'version' => '6.3.2.0',
|
||||||
|
'reference' => '70a298763b40b213ec087c51c739efcaa90bcd74',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../sebastian/exporter',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'sebastian/global-state' => array(
|
||||||
|
'pretty_version' => '7.0.2',
|
||||||
|
'version' => '7.0.2.0',
|
||||||
|
'reference' => '3be331570a721f9a4b5917f4209773de17f747d7',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../sebastian/global-state',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'sebastian/lines-of-code' => array(
|
||||||
|
'pretty_version' => '3.0.1',
|
||||||
|
'version' => '3.0.1.0',
|
||||||
|
'reference' => 'd36ad0d782e5756913e42ad87cb2890f4ffe467a',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../sebastian/lines-of-code',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'sebastian/object-enumerator' => array(
|
||||||
|
'pretty_version' => '6.0.1',
|
||||||
|
'version' => '6.0.1.0',
|
||||||
|
'reference' => 'f5b498e631a74204185071eb41f33f38d64608aa',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../sebastian/object-enumerator',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'sebastian/object-reflector' => array(
|
||||||
|
'pretty_version' => '4.0.1',
|
||||||
|
'version' => '4.0.1.0',
|
||||||
|
'reference' => '6e1a43b411b2ad34146dee7524cb13a068bb35f9',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../sebastian/object-reflector',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'sebastian/recursion-context' => array(
|
||||||
|
'pretty_version' => '6.0.3',
|
||||||
|
'version' => '6.0.3.0',
|
||||||
|
'reference' => 'f6458abbf32a6c8174f8f26261475dc133b3d9dc',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../sebastian/recursion-context',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'sebastian/type' => array(
|
||||||
|
'pretty_version' => '5.1.3',
|
||||||
|
'version' => '5.1.3.0',
|
||||||
|
'reference' => 'f77d2d4e78738c98d9a68d2596fe5e8fa380f449',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../sebastian/type',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'sebastian/version' => array(
|
||||||
|
'pretty_version' => '5.0.2',
|
||||||
|
'version' => '5.0.2.0',
|
||||||
|
'reference' => 'c687e3387b99f5b03b6caa64c74b63e2936ff874',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../sebastian/version',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
|
'staabm/side-effects-detector' => array(
|
||||||
|
'pretty_version' => '1.0.5',
|
||||||
|
'version' => '1.0.5.0',
|
||||||
|
'reference' => 'd8334211a140ce329c13726d4a715adbddd0a163',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../staabm/side-effects-detector',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
'symfony/deprecation-contracts' => array(
|
'symfony/deprecation-contracts' => array(
|
||||||
'pretty_version' => 'v3.6.0',
|
'pretty_version' => 'v3.6.0',
|
||||||
'version' => '3.6.0.0',
|
'version' => '3.6.0.0',
|
||||||
@@ -46,5 +280,14 @@
|
|||||||
'aliases' => array(),
|
'aliases' => array(),
|
||||||
'dev_requirement' => false,
|
'dev_requirement' => false,
|
||||||
),
|
),
|
||||||
|
'theseer/tokenizer' => array(
|
||||||
|
'pretty_version' => '1.3.1',
|
||||||
|
'version' => '1.3.1.0',
|
||||||
|
'reference' => 'b7489ce515e168639d17feec34b8847c326b0b3c',
|
||||||
|
'type' => 'library',
|
||||||
|
'install_path' => __DIR__ . '/../theseer/tokenizer',
|
||||||
|
'aliases' => array(),
|
||||||
|
'dev_requirement' => true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Vendored
+20
@@ -0,0 +1,20 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2013 My C-Sense
|
||||||
|
|
||||||
|
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.
|
||||||
Vendored
+406
@@ -0,0 +1,406 @@
|
|||||||
|
# DeepCopy
|
||||||
|
|
||||||
|
DeepCopy helps you create deep copies (clones) of your objects. It is designed to handle cycles in the association graph.
|
||||||
|
|
||||||
|
[](https://packagist.org/packages/myclabs/deep-copy)
|
||||||
|
[](https://github.com/myclabs/DeepCopy/actions/workflows/ci.yaml)
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [How](#how)
|
||||||
|
1. [Why](#why)
|
||||||
|
1. [Using simply `clone`](#using-simply-clone)
|
||||||
|
1. [Overriding `__clone()`](#overriding-__clone)
|
||||||
|
1. [With `DeepCopy`](#with-deepcopy)
|
||||||
|
1. [How it works](#how-it-works)
|
||||||
|
1. [Going further](#going-further)
|
||||||
|
1. [Matchers](#matchers)
|
||||||
|
1. [Property name](#property-name)
|
||||||
|
1. [Specific property](#specific-property)
|
||||||
|
1. [Type](#type)
|
||||||
|
1. [Filters](#filters)
|
||||||
|
1. [`SetNullFilter`](#setnullfilter-filter)
|
||||||
|
1. [`KeepFilter`](#keepfilter-filter)
|
||||||
|
1. [`DoctrineCollectionFilter`](#doctrinecollectionfilter-filter)
|
||||||
|
1. [`DoctrineEmptyCollectionFilter`](#doctrineemptycollectionfilter-filter)
|
||||||
|
1. [`DoctrineProxyFilter`](#doctrineproxyfilter-filter)
|
||||||
|
1. [`ReplaceFilter`](#replacefilter-type-filter)
|
||||||
|
1. [`ShallowCopyFilter`](#shallowcopyfilter-type-filter)
|
||||||
|
1. [Edge cases](#edge-cases)
|
||||||
|
1. [Contributing](#contributing)
|
||||||
|
1. [Tests](#tests)
|
||||||
|
|
||||||
|
|
||||||
|
## How?
|
||||||
|
|
||||||
|
Install with Composer:
|
||||||
|
|
||||||
|
```
|
||||||
|
composer require myclabs/deep-copy
|
||||||
|
```
|
||||||
|
|
||||||
|
Use it:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use DeepCopy\DeepCopy;
|
||||||
|
|
||||||
|
$copier = new DeepCopy();
|
||||||
|
$myCopy = $copier->copy($myObject);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Why?
|
||||||
|
|
||||||
|
- How do you create copies of your objects?
|
||||||
|
|
||||||
|
```php
|
||||||
|
$myCopy = clone $myObject;
|
||||||
|
```
|
||||||
|
|
||||||
|
- How do you create **deep** copies of your objects (i.e. copying also all the objects referenced in the properties)?
|
||||||
|
|
||||||
|
You use [`__clone()`](http://www.php.net/manual/en/language.oop5.cloning.php#object.clone) and implement the behavior
|
||||||
|
yourself.
|
||||||
|
|
||||||
|
- But how do you handle **cycles** in the association graph?
|
||||||
|
|
||||||
|
Now you're in for a big mess :(
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
### Using simply `clone`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
### Overriding `__clone()`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
### With `DeepCopy`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
DeepCopy recursively traverses all the object's properties and clones them. To avoid cloning the same object twice it
|
||||||
|
keeps a hash map of all instances and thus preserves the object graph.
|
||||||
|
|
||||||
|
To use it:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use function DeepCopy\deep_copy;
|
||||||
|
|
||||||
|
$copy = deep_copy($var);
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can create your own `DeepCopy` instance to configure it differently for example:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use DeepCopy\DeepCopy;
|
||||||
|
|
||||||
|
$copier = new DeepCopy(true);
|
||||||
|
|
||||||
|
$copy = $copier->copy($var);
|
||||||
|
```
|
||||||
|
|
||||||
|
You may want to roll your own deep copy function:
|
||||||
|
|
||||||
|
```php
|
||||||
|
namespace Acme;
|
||||||
|
|
||||||
|
use DeepCopy\DeepCopy;
|
||||||
|
|
||||||
|
function deep_copy($var)
|
||||||
|
{
|
||||||
|
static $copier = null;
|
||||||
|
|
||||||
|
if (null === $copier) {
|
||||||
|
$copier = new DeepCopy(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $copier->copy($var);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Going further
|
||||||
|
|
||||||
|
You can add filters to customize the copy process.
|
||||||
|
|
||||||
|
The method to add a filter is `DeepCopy\DeepCopy::addFilter($filter, $matcher)`,
|
||||||
|
with `$filter` implementing `DeepCopy\Filter\Filter`
|
||||||
|
and `$matcher` implementing `DeepCopy\Matcher\Matcher`.
|
||||||
|
|
||||||
|
We provide some generic filters and matchers.
|
||||||
|
|
||||||
|
|
||||||
|
### Matchers
|
||||||
|
|
||||||
|
- `DeepCopy\Matcher` applies on a object attribute.
|
||||||
|
- `DeepCopy\TypeMatcher` applies on any element found in graph, including array elements.
|
||||||
|
|
||||||
|
|
||||||
|
#### Property name
|
||||||
|
|
||||||
|
The `PropertyNameMatcher` will match a property by its name:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use DeepCopy\Matcher\PropertyNameMatcher;
|
||||||
|
|
||||||
|
// Will apply a filter to any property of any objects named "id"
|
||||||
|
$matcher = new PropertyNameMatcher('id');
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Specific property
|
||||||
|
|
||||||
|
The `PropertyMatcher` will match a specific property of a specific class:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use DeepCopy\Matcher\PropertyMatcher;
|
||||||
|
|
||||||
|
// Will apply a filter to the property "id" of any objects of the class "MyClass"
|
||||||
|
$matcher = new PropertyMatcher('MyClass', 'id');
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Type
|
||||||
|
|
||||||
|
The `TypeMatcher` will match any element by its type (instance of a class or any value that could be parameter of
|
||||||
|
[gettype()](http://php.net/manual/en/function.gettype.php) function):
|
||||||
|
|
||||||
|
```php
|
||||||
|
use DeepCopy\TypeMatcher\TypeMatcher;
|
||||||
|
|
||||||
|
// Will apply a filter to any object that is an instance of Doctrine\Common\Collections\Collection
|
||||||
|
$matcher = new TypeMatcher('Doctrine\Common\Collections\Collection');
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Filters
|
||||||
|
|
||||||
|
- `DeepCopy\Filter` applies a transformation to the object attribute matched by `DeepCopy\Matcher`
|
||||||
|
- `DeepCopy\TypeFilter` applies a transformation to any element matched by `DeepCopy\TypeMatcher`
|
||||||
|
|
||||||
|
By design, matching a filter will stop the chain of filters (i.e. the next ones will not be applied).
|
||||||
|
Using the ([`ChainableFilter`](#chainablefilter-filter)) won't stop the chain of filters.
|
||||||
|
|
||||||
|
|
||||||
|
#### `SetNullFilter` (filter)
|
||||||
|
|
||||||
|
Let's say for example that you are copying a database record (or a Doctrine entity), so you want the copy not to have
|
||||||
|
any ID:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use DeepCopy\DeepCopy;
|
||||||
|
use DeepCopy\Filter\SetNullFilter;
|
||||||
|
use DeepCopy\Matcher\PropertyNameMatcher;
|
||||||
|
|
||||||
|
$object = MyClass::load(123);
|
||||||
|
echo $object->id; // 123
|
||||||
|
|
||||||
|
$copier = new DeepCopy();
|
||||||
|
$copier->addFilter(new SetNullFilter(), new PropertyNameMatcher('id'));
|
||||||
|
|
||||||
|
$copy = $copier->copy($object);
|
||||||
|
|
||||||
|
echo $copy->id; // null
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### `KeepFilter` (filter)
|
||||||
|
|
||||||
|
If you want a property to remain untouched (for example, an association to an object):
|
||||||
|
|
||||||
|
```php
|
||||||
|
use DeepCopy\DeepCopy;
|
||||||
|
use DeepCopy\Filter\KeepFilter;
|
||||||
|
use DeepCopy\Matcher\PropertyMatcher;
|
||||||
|
|
||||||
|
$copier = new DeepCopy();
|
||||||
|
$copier->addFilter(new KeepFilter(), new PropertyMatcher('MyClass', 'category'));
|
||||||
|
|
||||||
|
$copy = $copier->copy($object);
|
||||||
|
// $copy->category has not been touched
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### `ChainableFilter` (filter)
|
||||||
|
|
||||||
|
If you use cloning on proxy classes, you might want to apply two filters for:
|
||||||
|
1. loading the data
|
||||||
|
2. applying a transformation
|
||||||
|
|
||||||
|
You can use the `ChainableFilter` as a decorator of the proxy loader filter, which won't stop the chain of filters (i.e.
|
||||||
|
the next ones may be applied).
|
||||||
|
|
||||||
|
|
||||||
|
```php
|
||||||
|
use DeepCopy\DeepCopy;
|
||||||
|
use DeepCopy\Filter\ChainableFilter;
|
||||||
|
use DeepCopy\Filter\Doctrine\DoctrineProxyFilter;
|
||||||
|
use DeepCopy\Filter\SetNullFilter;
|
||||||
|
use DeepCopy\Matcher\Doctrine\DoctrineProxyMatcher;
|
||||||
|
use DeepCopy\Matcher\PropertyNameMatcher;
|
||||||
|
|
||||||
|
$copier = new DeepCopy();
|
||||||
|
$copier->addFilter(new ChainableFilter(new DoctrineProxyFilter()), new DoctrineProxyMatcher());
|
||||||
|
$copier->addFilter(new SetNullFilter(), new PropertyNameMatcher('id'));
|
||||||
|
|
||||||
|
$copy = $copier->copy($object);
|
||||||
|
|
||||||
|
echo $copy->id; // null
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### `DoctrineCollectionFilter` (filter)
|
||||||
|
|
||||||
|
If you use Doctrine and want to copy an entity, you will need to use the `DoctrineCollectionFilter`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use DeepCopy\DeepCopy;
|
||||||
|
use DeepCopy\Filter\Doctrine\DoctrineCollectionFilter;
|
||||||
|
use DeepCopy\Matcher\PropertyTypeMatcher;
|
||||||
|
|
||||||
|
$copier = new DeepCopy();
|
||||||
|
$copier->addFilter(new DoctrineCollectionFilter(), new PropertyTypeMatcher('Doctrine\Common\Collections\Collection'));
|
||||||
|
|
||||||
|
$copy = $copier->copy($object);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### `DoctrineEmptyCollectionFilter` (filter)
|
||||||
|
|
||||||
|
If you use Doctrine and want to copy an entity who contains a `Collection` that you want to be reset, you can use the
|
||||||
|
`DoctrineEmptyCollectionFilter`
|
||||||
|
|
||||||
|
```php
|
||||||
|
use DeepCopy\DeepCopy;
|
||||||
|
use DeepCopy\Filter\Doctrine\DoctrineEmptyCollectionFilter;
|
||||||
|
use DeepCopy\Matcher\PropertyMatcher;
|
||||||
|
|
||||||
|
$copier = new DeepCopy();
|
||||||
|
$copier->addFilter(new DoctrineEmptyCollectionFilter(), new PropertyMatcher('MyClass', 'myProperty'));
|
||||||
|
|
||||||
|
$copy = $copier->copy($object);
|
||||||
|
|
||||||
|
// $copy->myProperty will return an empty collection
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### `DoctrineProxyFilter` (filter)
|
||||||
|
|
||||||
|
If you use Doctrine and use cloning on lazy loaded entities, you might encounter errors mentioning missing fields on a
|
||||||
|
Doctrine proxy class (...\\\_\_CG\_\_\Proxy).
|
||||||
|
You can use the `DoctrineProxyFilter` to load the actual entity behind the Doctrine proxy class.
|
||||||
|
**Make sure, though, to put this as one of your very first filters in the filter chain so that the entity is loaded
|
||||||
|
before other filters are applied!**
|
||||||
|
We recommend to decorate the `DoctrineProxyFilter` with the `ChainableFilter` to allow applying other filters to the
|
||||||
|
cloned lazy loaded entities.
|
||||||
|
|
||||||
|
```php
|
||||||
|
use DeepCopy\DeepCopy;
|
||||||
|
use DeepCopy\Filter\Doctrine\DoctrineProxyFilter;
|
||||||
|
use DeepCopy\Matcher\Doctrine\DoctrineProxyMatcher;
|
||||||
|
|
||||||
|
$copier = new DeepCopy();
|
||||||
|
$copier->addFilter(new ChainableFilter(new DoctrineProxyFilter()), new DoctrineProxyMatcher());
|
||||||
|
|
||||||
|
$copy = $copier->copy($object);
|
||||||
|
|
||||||
|
// $copy should now contain a clone of all entities, including those that were not yet fully loaded.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### `ReplaceFilter` (type filter)
|
||||||
|
|
||||||
|
1. If you want to replace the value of a property:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use DeepCopy\DeepCopy;
|
||||||
|
use DeepCopy\Filter\ReplaceFilter;
|
||||||
|
use DeepCopy\Matcher\PropertyMatcher;
|
||||||
|
|
||||||
|
$copier = new DeepCopy();
|
||||||
|
$callback = function ($currentValue) {
|
||||||
|
return $currentValue . ' (copy)'
|
||||||
|
};
|
||||||
|
$copier->addFilter(new ReplaceFilter($callback), new PropertyMatcher('MyClass', 'title'));
|
||||||
|
|
||||||
|
$copy = $copier->copy($object);
|
||||||
|
|
||||||
|
// $copy->title will contain the data returned by the callback, e.g. 'The title (copy)'
|
||||||
|
```
|
||||||
|
|
||||||
|
2. If you want to replace whole element:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use DeepCopy\DeepCopy;
|
||||||
|
use DeepCopy\TypeFilter\ReplaceFilter;
|
||||||
|
use DeepCopy\TypeMatcher\TypeMatcher;
|
||||||
|
|
||||||
|
$copier = new DeepCopy();
|
||||||
|
$callback = function (MyClass $myClass) {
|
||||||
|
return get_class($myClass);
|
||||||
|
};
|
||||||
|
$copier->addTypeFilter(new ReplaceFilter($callback), new TypeMatcher('MyClass'));
|
||||||
|
|
||||||
|
$copy = $copier->copy([new MyClass, 'some string', new MyClass]);
|
||||||
|
|
||||||
|
// $copy will contain ['MyClass', 'some string', 'MyClass']
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The `$callback` parameter of the `ReplaceFilter` constructor accepts any PHP callable.
|
||||||
|
|
||||||
|
|
||||||
|
#### `ShallowCopyFilter` (type filter)
|
||||||
|
|
||||||
|
Stop *DeepCopy* from recursively copying element, using standard `clone` instead:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use DeepCopy\DeepCopy;
|
||||||
|
use DeepCopy\TypeFilter\ShallowCopyFilter;
|
||||||
|
use DeepCopy\TypeMatcher\TypeMatcher;
|
||||||
|
use Mockery as m;
|
||||||
|
|
||||||
|
$this->deepCopy = new DeepCopy();
|
||||||
|
$this->deepCopy->addTypeFilter(
|
||||||
|
new ShallowCopyFilter,
|
||||||
|
new TypeMatcher(m\MockInterface::class)
|
||||||
|
);
|
||||||
|
|
||||||
|
$myServiceWithMocks = new MyService(m::mock(MyDependency1::class), m::mock(MyDependency2::class));
|
||||||
|
// All mocks will be just cloned, not deep copied
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Edge cases
|
||||||
|
|
||||||
|
The following structures cannot be deep-copied with PHP Reflection. As a result they are shallow cloned and filters are
|
||||||
|
not applied. There is two ways for you to handle them:
|
||||||
|
|
||||||
|
- Implement your own `__clone()` method
|
||||||
|
- Use a filter with a type matcher
|
||||||
|
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
DeepCopy is distributed under the MIT license.
|
||||||
|
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
Running the tests is simple:
|
||||||
|
|
||||||
|
```php
|
||||||
|
vendor/bin/phpunit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Support
|
||||||
|
|
||||||
|
Get professional support via [the Tidelift Subscription](https://tidelift.com/subscription/pkg/packagist-myclabs-deep-copy?utm_source=packagist-myclabs-deep-copy&utm_medium=referral&utm_campaign=readme).
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "myclabs/deep-copy",
|
||||||
|
"description": "Create deep copies (clones) of your objects",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "library",
|
||||||
|
"keywords": [
|
||||||
|
"clone",
|
||||||
|
"copy",
|
||||||
|
"duplicate",
|
||||||
|
"object",
|
||||||
|
"object graph"
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"doctrine/collections": "^1.6.8",
|
||||||
|
"doctrine/common": "^2.13.3 || ^3.2.2",
|
||||||
|
"phpspec/prophecy": "^1.10",
|
||||||
|
"phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"doctrine/collections": "<1.6.8",
|
||||||
|
"doctrine/common": "<2.13.3 || >=3 <3.2.2"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"DeepCopy\\": "src/DeepCopy/"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/DeepCopy/deep_copy.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"DeepCopyTest\\": "tests/DeepCopyTest/",
|
||||||
|
"DeepCopy\\": "fixtures/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"sort-packages": true
|
||||||
|
}
|
||||||
|
}
|
||||||
+328
@@ -0,0 +1,328 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy;
|
||||||
|
|
||||||
|
use ArrayObject;
|
||||||
|
use DateInterval;
|
||||||
|
use DatePeriod;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use DateTimeZone;
|
||||||
|
use DeepCopy\Exception\CloneException;
|
||||||
|
use DeepCopy\Filter\ChainableFilter;
|
||||||
|
use DeepCopy\Filter\Filter;
|
||||||
|
use DeepCopy\Matcher\Matcher;
|
||||||
|
use DeepCopy\Reflection\ReflectionHelper;
|
||||||
|
use DeepCopy\TypeFilter\Date\DateIntervalFilter;
|
||||||
|
use DeepCopy\TypeFilter\Date\DatePeriodFilter;
|
||||||
|
use DeepCopy\TypeFilter\Spl\ArrayObjectFilter;
|
||||||
|
use DeepCopy\TypeFilter\Spl\SplDoublyLinkedListFilter;
|
||||||
|
use DeepCopy\TypeFilter\TypeFilter;
|
||||||
|
use DeepCopy\TypeMatcher\TypeMatcher;
|
||||||
|
use ReflectionObject;
|
||||||
|
use ReflectionProperty;
|
||||||
|
use SplDoublyLinkedList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @final
|
||||||
|
*/
|
||||||
|
class DeepCopy
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var object[] List of objects copied.
|
||||||
|
*/
|
||||||
|
private $hashMap = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters to apply.
|
||||||
|
*
|
||||||
|
* @var array Array of ['filter' => Filter, 'matcher' => Matcher] pairs.
|
||||||
|
*/
|
||||||
|
private $filters = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type Filters to apply.
|
||||||
|
*
|
||||||
|
* @var array Array of ['filter' => Filter, 'matcher' => Matcher] pairs.
|
||||||
|
*/
|
||||||
|
private $typeFilters = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private $skipUncloneable = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private $useCloneMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param bool $useCloneMethod If set to true, when an object implements the __clone() function, it will be used
|
||||||
|
* instead of the regular deep cloning.
|
||||||
|
*/
|
||||||
|
public function __construct($useCloneMethod = false)
|
||||||
|
{
|
||||||
|
$this->useCloneMethod = $useCloneMethod;
|
||||||
|
|
||||||
|
$this->addTypeFilter(new ArrayObjectFilter($this), new TypeMatcher(ArrayObject::class));
|
||||||
|
$this->addTypeFilter(new DateIntervalFilter(), new TypeMatcher(DateInterval::class));
|
||||||
|
$this->addTypeFilter(new DatePeriodFilter(), new TypeMatcher(DatePeriod::class));
|
||||||
|
$this->addTypeFilter(new SplDoublyLinkedListFilter($this), new TypeMatcher(SplDoublyLinkedList::class));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If enabled, will not throw an exception when coming across an uncloneable property.
|
||||||
|
*
|
||||||
|
* @param $skipUncloneable
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function skipUncloneable($skipUncloneable = true)
|
||||||
|
{
|
||||||
|
$this->skipUncloneable = $skipUncloneable;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deep copies the given object.
|
||||||
|
*
|
||||||
|
* @template TObject
|
||||||
|
*
|
||||||
|
* @param TObject $object
|
||||||
|
*
|
||||||
|
* @return TObject
|
||||||
|
*/
|
||||||
|
public function copy($object)
|
||||||
|
{
|
||||||
|
$this->hashMap = [];
|
||||||
|
|
||||||
|
return $this->recursiveCopy($object);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addFilter(Filter $filter, Matcher $matcher)
|
||||||
|
{
|
||||||
|
$this->filters[] = [
|
||||||
|
'matcher' => $matcher,
|
||||||
|
'filter' => $filter,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function prependFilter(Filter $filter, Matcher $matcher)
|
||||||
|
{
|
||||||
|
array_unshift($this->filters, [
|
||||||
|
'matcher' => $matcher,
|
||||||
|
'filter' => $filter,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addTypeFilter(TypeFilter $filter, TypeMatcher $matcher)
|
||||||
|
{
|
||||||
|
$this->typeFilters[] = [
|
||||||
|
'matcher' => $matcher,
|
||||||
|
'filter' => $filter,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function prependTypeFilter(TypeFilter $filter, TypeMatcher $matcher)
|
||||||
|
{
|
||||||
|
array_unshift($this->typeFilters, [
|
||||||
|
'matcher' => $matcher,
|
||||||
|
'filter' => $filter,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function recursiveCopy($var)
|
||||||
|
{
|
||||||
|
// Matches Type Filter
|
||||||
|
if ($filter = $this->getFirstMatchedTypeFilter($this->typeFilters, $var)) {
|
||||||
|
return $filter->apply($var);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource
|
||||||
|
if (is_resource($var)) {
|
||||||
|
return $var;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Array
|
||||||
|
if (is_array($var)) {
|
||||||
|
return $this->copyArray($var);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scalar
|
||||||
|
if (! is_object($var)) {
|
||||||
|
return $var;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enum
|
||||||
|
if (PHP_VERSION_ID >= 80100 && enum_exists(get_class($var))) {
|
||||||
|
return $var;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object
|
||||||
|
return $this->copyObject($var);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy an array
|
||||||
|
* @param array $array
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function copyArray(array $array)
|
||||||
|
{
|
||||||
|
foreach ($array as $key => $value) {
|
||||||
|
$array[$key] = $this->recursiveCopy($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $array;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies an object.
|
||||||
|
*
|
||||||
|
* @param object $object
|
||||||
|
*
|
||||||
|
* @throws CloneException
|
||||||
|
*
|
||||||
|
* @return object
|
||||||
|
*/
|
||||||
|
private function copyObject($object)
|
||||||
|
{
|
||||||
|
$objectHash = spl_object_hash($object);
|
||||||
|
|
||||||
|
if (isset($this->hashMap[$objectHash])) {
|
||||||
|
return $this->hashMap[$objectHash];
|
||||||
|
}
|
||||||
|
|
||||||
|
$reflectedObject = new ReflectionObject($object);
|
||||||
|
$isCloneable = $reflectedObject->isCloneable();
|
||||||
|
|
||||||
|
if (false === $isCloneable) {
|
||||||
|
if ($this->skipUncloneable) {
|
||||||
|
$this->hashMap[$objectHash] = $object;
|
||||||
|
|
||||||
|
return $object;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new CloneException(
|
||||||
|
sprintf(
|
||||||
|
'The class "%s" is not cloneable.',
|
||||||
|
$reflectedObject->getName()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$newObject = clone $object;
|
||||||
|
$this->hashMap[$objectHash] = $newObject;
|
||||||
|
|
||||||
|
if ($this->useCloneMethod && $reflectedObject->hasMethod('__clone')) {
|
||||||
|
return $newObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($newObject instanceof DateTimeInterface || $newObject instanceof DateTimeZone) {
|
||||||
|
return $newObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (ReflectionHelper::getProperties($reflectedObject) as $property) {
|
||||||
|
$this->copyObjectProperty($newObject, $property);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $newObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function copyObjectProperty($object, ReflectionProperty $property)
|
||||||
|
{
|
||||||
|
// Ignore static properties
|
||||||
|
if ($property->isStatic()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore readonly properties
|
||||||
|
if (method_exists($property, 'isReadOnly') && $property->isReadOnly()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the filters
|
||||||
|
foreach ($this->filters as $item) {
|
||||||
|
/** @var Matcher $matcher */
|
||||||
|
$matcher = $item['matcher'];
|
||||||
|
/** @var Filter $filter */
|
||||||
|
$filter = $item['filter'];
|
||||||
|
|
||||||
|
if ($matcher->matches($object, $property->getName())) {
|
||||||
|
$filter->apply(
|
||||||
|
$object,
|
||||||
|
$property->getName(),
|
||||||
|
function ($object) {
|
||||||
|
return $this->recursiveCopy($object);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($filter instanceof ChainableFilter) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a filter matches, we stop processing this property
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 80100) {
|
||||||
|
$property->setAccessible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore uninitialized properties (for PHP >7.4)
|
||||||
|
if (method_exists($property, 'isInitialized') && !$property->isInitialized($object)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$propertyValue = $property->getValue($object);
|
||||||
|
|
||||||
|
// Copy the property
|
||||||
|
$property->setValue($object, $this->recursiveCopy($propertyValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns first filter that matches variable, `null` if no such filter found.
|
||||||
|
*
|
||||||
|
* @param array $filterRecords Associative array with 2 members: 'filter' with value of type {@see TypeFilter} and
|
||||||
|
* 'matcher' with value of type {@see TypeMatcher}
|
||||||
|
* @param mixed $var
|
||||||
|
*
|
||||||
|
* @return TypeFilter|null
|
||||||
|
*/
|
||||||
|
private function getFirstMatchedTypeFilter(array $filterRecords, $var)
|
||||||
|
{
|
||||||
|
$matched = $this->first(
|
||||||
|
$filterRecords,
|
||||||
|
function (array $record) use ($var) {
|
||||||
|
/* @var TypeMatcher $matcher */
|
||||||
|
$matcher = $record['matcher'];
|
||||||
|
|
||||||
|
return $matcher->matches($var);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return isset($matched) ? $matched['filter'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns first element that matches predicate, `null` if no such element found.
|
||||||
|
*
|
||||||
|
* @param array $elements Array of ['filter' => Filter, 'matcher' => Matcher] pairs.
|
||||||
|
* @param callable $predicate Predicate arguments are: element.
|
||||||
|
*
|
||||||
|
* @return array|null Associative array with 2 members: 'filter' with value of type {@see TypeFilter} and 'matcher'
|
||||||
|
* with value of type {@see TypeMatcher} or `null`.
|
||||||
|
*/
|
||||||
|
private function first(array $elements, callable $predicate)
|
||||||
|
{
|
||||||
|
foreach ($elements as $element) {
|
||||||
|
if (call_user_func($predicate, $element)) {
|
||||||
|
return $element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\Exception;
|
||||||
|
|
||||||
|
use UnexpectedValueException;
|
||||||
|
|
||||||
|
class CloneException extends UnexpectedValueException
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\Exception;
|
||||||
|
|
||||||
|
use ReflectionException;
|
||||||
|
|
||||||
|
class PropertyException extends ReflectionException
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\Filter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a decorator filter that will not stop the chain of filters.
|
||||||
|
*/
|
||||||
|
class ChainableFilter implements Filter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Filter
|
||||||
|
*/
|
||||||
|
protected $filter;
|
||||||
|
|
||||||
|
public function __construct(Filter $filter)
|
||||||
|
{
|
||||||
|
$this->filter = $filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function apply($object, $property, $objectCopier)
|
||||||
|
{
|
||||||
|
$this->filter->apply($object, $property, $objectCopier);
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\Filter\Doctrine;
|
||||||
|
|
||||||
|
use DeepCopy\Filter\Filter;
|
||||||
|
use DeepCopy\Reflection\ReflectionHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @final
|
||||||
|
*/
|
||||||
|
class DoctrineCollectionFilter implements Filter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Copies the object property doctrine collection.
|
||||||
|
*
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function apply($object, $property, $objectCopier)
|
||||||
|
{
|
||||||
|
$reflectionProperty = ReflectionHelper::getProperty($object, $property);
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 80100) {
|
||||||
|
$reflectionProperty->setAccessible(true);
|
||||||
|
}
|
||||||
|
$oldCollection = $reflectionProperty->getValue($object);
|
||||||
|
|
||||||
|
$newCollection = $oldCollection->map(
|
||||||
|
function ($item) use ($objectCopier) {
|
||||||
|
return $objectCopier($item);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$reflectionProperty->setValue($object, $newCollection);
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\Filter\Doctrine;
|
||||||
|
|
||||||
|
use DeepCopy\Filter\Filter;
|
||||||
|
use DeepCopy\Reflection\ReflectionHelper;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @final
|
||||||
|
*/
|
||||||
|
class DoctrineEmptyCollectionFilter implements Filter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Sets the object property to an empty doctrine collection.
|
||||||
|
*
|
||||||
|
* @param object $object
|
||||||
|
* @param string $property
|
||||||
|
* @param callable $objectCopier
|
||||||
|
*/
|
||||||
|
public function apply($object, $property, $objectCopier)
|
||||||
|
{
|
||||||
|
$reflectionProperty = ReflectionHelper::getProperty($object, $property);
|
||||||
|
if (PHP_VERSION_ID < 80100) {
|
||||||
|
$reflectionProperty->setAccessible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$reflectionProperty->setValue($object, new ArrayCollection());
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\Filter\Doctrine;
|
||||||
|
|
||||||
|
use DeepCopy\Filter\Filter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @final
|
||||||
|
*/
|
||||||
|
class DoctrineProxyFilter implements Filter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Triggers the magic method __load() on a Doctrine Proxy class to load the
|
||||||
|
* actual entity from the database.
|
||||||
|
*
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function apply($object, $property, $objectCopier)
|
||||||
|
{
|
||||||
|
$object->__load();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\Filter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter to apply to a property while copying an object
|
||||||
|
*/
|
||||||
|
interface Filter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Applies the filter to the object.
|
||||||
|
*
|
||||||
|
* @param object $object
|
||||||
|
* @param string $property
|
||||||
|
* @param callable $objectCopier
|
||||||
|
*/
|
||||||
|
public function apply($object, $property, $objectCopier);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\Filter;
|
||||||
|
|
||||||
|
class KeepFilter implements Filter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Keeps the value of the object property.
|
||||||
|
*
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function apply($object, $property, $objectCopier)
|
||||||
|
{
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\Filter;
|
||||||
|
|
||||||
|
use DeepCopy\Reflection\ReflectionHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @final
|
||||||
|
*/
|
||||||
|
class ReplaceFilter implements Filter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var callable
|
||||||
|
*/
|
||||||
|
protected $callback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param callable $callable Will be called to get the new value for each property to replace
|
||||||
|
*/
|
||||||
|
public function __construct(callable $callable)
|
||||||
|
{
|
||||||
|
$this->callback = $callable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the object property by the result of the callback called with the object property.
|
||||||
|
*
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function apply($object, $property, $objectCopier)
|
||||||
|
{
|
||||||
|
$reflectionProperty = ReflectionHelper::getProperty($object, $property);
|
||||||
|
if (PHP_VERSION_ID < 80100) {
|
||||||
|
$reflectionProperty->setAccessible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = call_user_func($this->callback, $reflectionProperty->getValue($object));
|
||||||
|
|
||||||
|
$reflectionProperty->setValue($object, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\Filter;
|
||||||
|
|
||||||
|
use DeepCopy\Reflection\ReflectionHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @final
|
||||||
|
*/
|
||||||
|
class SetNullFilter implements Filter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Sets the object property to null.
|
||||||
|
*
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function apply($object, $property, $objectCopier)
|
||||||
|
{
|
||||||
|
$reflectionProperty = ReflectionHelper::getProperty($object, $property);
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 80100) {
|
||||||
|
$reflectionProperty->setAccessible(true);
|
||||||
|
}
|
||||||
|
$reflectionProperty->setValue($object, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\Matcher\Doctrine;
|
||||||
|
|
||||||
|
use DeepCopy\Matcher\Matcher;
|
||||||
|
use Doctrine\Persistence\Proxy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @final
|
||||||
|
*/
|
||||||
|
class DoctrineProxyMatcher implements Matcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Matches a Doctrine Proxy class.
|
||||||
|
*
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function matches($object, $property)
|
||||||
|
{
|
||||||
|
return $object instanceof Proxy;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\Matcher;
|
||||||
|
|
||||||
|
interface Matcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param object $object
|
||||||
|
* @param string $property
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function matches($object, $property);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\Matcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @final
|
||||||
|
*/
|
||||||
|
class PropertyMatcher implements Matcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $class;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $property;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $class Class name
|
||||||
|
* @param string $property Property name
|
||||||
|
*/
|
||||||
|
public function __construct($class, $property)
|
||||||
|
{
|
||||||
|
$this->class = $class;
|
||||||
|
$this->property = $property;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a specific property of a specific class.
|
||||||
|
*
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function matches($object, $property)
|
||||||
|
{
|
||||||
|
return ($object instanceof $this->class) && $property == $this->property;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\Matcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @final
|
||||||
|
*/
|
||||||
|
class PropertyNameMatcher implements Matcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $property;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $property Property name
|
||||||
|
*/
|
||||||
|
public function __construct($property)
|
||||||
|
{
|
||||||
|
$this->property = $property;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a property by its name.
|
||||||
|
*
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function matches($object, $property)
|
||||||
|
{
|
||||||
|
return $property == $this->property;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\Matcher;
|
||||||
|
|
||||||
|
use DeepCopy\Reflection\ReflectionHelper;
|
||||||
|
use ReflectionException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a property by its type.
|
||||||
|
*
|
||||||
|
* It is recommended to use {@see DeepCopy\TypeFilter\TypeFilter} instead, as it applies on all occurrences
|
||||||
|
* of given type in copied context (eg. array elements), not just on object properties.
|
||||||
|
*
|
||||||
|
* @final
|
||||||
|
*/
|
||||||
|
class PropertyTypeMatcher implements Matcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $propertyType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $propertyType Property type
|
||||||
|
*/
|
||||||
|
public function __construct($propertyType)
|
||||||
|
{
|
||||||
|
$this->propertyType = $propertyType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function matches($object, $property)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$reflectionProperty = ReflectionHelper::getProperty($object, $property);
|
||||||
|
} catch (ReflectionException $exception) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 80100) {
|
||||||
|
$reflectionProperty->setAccessible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uninitialized properties (for PHP >7.4)
|
||||||
|
if (method_exists($reflectionProperty, 'isInitialized') && !$reflectionProperty->isInitialized($object)) {
|
||||||
|
// null instanceof $this->propertyType
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $reflectionProperty->getValue($object) instanceof $this->propertyType;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\Reflection;
|
||||||
|
|
||||||
|
use DeepCopy\Exception\PropertyException;
|
||||||
|
use ReflectionClass;
|
||||||
|
use ReflectionException;
|
||||||
|
use ReflectionObject;
|
||||||
|
use ReflectionProperty;
|
||||||
|
|
||||||
|
class ReflectionHelper
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Retrieves all properties (including private ones), from object and all its ancestors.
|
||||||
|
*
|
||||||
|
* Standard \ReflectionClass->getProperties() does not return private properties from ancestor classes.
|
||||||
|
*
|
||||||
|
* @author muratyaman@gmail.com
|
||||||
|
* @see http://php.net/manual/en/reflectionclass.getproperties.php
|
||||||
|
*
|
||||||
|
* @param ReflectionClass $ref
|
||||||
|
*
|
||||||
|
* @return ReflectionProperty[]
|
||||||
|
*/
|
||||||
|
public static function getProperties(ReflectionClass $ref)
|
||||||
|
{
|
||||||
|
$props = $ref->getProperties();
|
||||||
|
$propsArr = array();
|
||||||
|
|
||||||
|
foreach ($props as $prop) {
|
||||||
|
$propertyName = $prop->getName();
|
||||||
|
$propsArr[$propertyName] = $prop;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($parentClass = $ref->getParentClass()) {
|
||||||
|
$parentPropsArr = self::getProperties($parentClass);
|
||||||
|
foreach ($propsArr as $key => $property) {
|
||||||
|
$parentPropsArr[$key] = $property;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $parentPropsArr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $propsArr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves property by name from object and all its ancestors.
|
||||||
|
*
|
||||||
|
* @param object|string $object
|
||||||
|
* @param string $name
|
||||||
|
*
|
||||||
|
* @throws PropertyException
|
||||||
|
* @throws ReflectionException
|
||||||
|
*
|
||||||
|
* @return ReflectionProperty
|
||||||
|
*/
|
||||||
|
public static function getProperty($object, $name)
|
||||||
|
{
|
||||||
|
$reflection = is_object($object) ? new ReflectionObject($object) : new ReflectionClass($object);
|
||||||
|
|
||||||
|
if ($reflection->hasProperty($name)) {
|
||||||
|
return $reflection->getProperty($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($parentClass = $reflection->getParentClass()) {
|
||||||
|
return self::getProperty($parentClass->getName(), $name);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new PropertyException(
|
||||||
|
sprintf(
|
||||||
|
'The class "%s" doesn\'t have a property with the given name: "%s".',
|
||||||
|
is_object($object) ? get_class($object) : $object,
|
||||||
|
$name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\TypeFilter\Date;
|
||||||
|
|
||||||
|
use DateInterval;
|
||||||
|
use DeepCopy\TypeFilter\TypeFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @final
|
||||||
|
*
|
||||||
|
* @deprecated Will be removed in 2.0. This filter will no longer be necessary in PHP 7.1+.
|
||||||
|
*/
|
||||||
|
class DateIntervalFilter implements TypeFilter
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @param DateInterval $element
|
||||||
|
*
|
||||||
|
* @see http://news.php.net/php.bugs/205076
|
||||||
|
*/
|
||||||
|
public function apply($element)
|
||||||
|
{
|
||||||
|
$copy = new DateInterval('P0D');
|
||||||
|
|
||||||
|
foreach ($element as $propertyName => $propertyValue) {
|
||||||
|
$copy->{$propertyName} = $propertyValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $copy;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\TypeFilter\Date;
|
||||||
|
|
||||||
|
use DatePeriod;
|
||||||
|
use DeepCopy\TypeFilter\TypeFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @final
|
||||||
|
*/
|
||||||
|
class DatePeriodFilter implements TypeFilter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @param DatePeriod $element
|
||||||
|
*
|
||||||
|
* @see http://news.php.net/php.bugs/205076
|
||||||
|
*/
|
||||||
|
public function apply($element)
|
||||||
|
{
|
||||||
|
$options = 0;
|
||||||
|
if (PHP_VERSION_ID >= 80200 && $element->include_end_date) {
|
||||||
|
$options |= DatePeriod::INCLUDE_END_DATE;
|
||||||
|
}
|
||||||
|
if (!$element->include_start_date) {
|
||||||
|
$options |= DatePeriod::EXCLUDE_START_DATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($element->getEndDate()) {
|
||||||
|
return new DatePeriod($element->getStartDate(), $element->getDateInterval(), $element->getEndDate(), $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID >= 70217) {
|
||||||
|
$recurrences = $element->getRecurrences();
|
||||||
|
} else {
|
||||||
|
$recurrences = $element->recurrences - $element->include_start_date;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DatePeriod($element->getStartDate(), $element->getDateInterval(), $recurrences, $options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\TypeFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @final
|
||||||
|
*/
|
||||||
|
class ReplaceFilter implements TypeFilter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var callable
|
||||||
|
*/
|
||||||
|
protected $callback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param callable $callable Will be called to get the new value for each element to replace
|
||||||
|
*/
|
||||||
|
public function __construct(callable $callable)
|
||||||
|
{
|
||||||
|
$this->callback = $callable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function apply($element)
|
||||||
|
{
|
||||||
|
return call_user_func($this->callback, $element);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\TypeFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @final
|
||||||
|
*/
|
||||||
|
class ShallowCopyFilter implements TypeFilter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function apply($element)
|
||||||
|
{
|
||||||
|
return clone $element;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
namespace DeepCopy\TypeFilter\Spl;
|
||||||
|
|
||||||
|
use DeepCopy\DeepCopy;
|
||||||
|
use DeepCopy\TypeFilter\TypeFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In PHP 7.4 the storage of an ArrayObject isn't returned as
|
||||||
|
* ReflectionProperty. So we deep copy its array copy.
|
||||||
|
*/
|
||||||
|
final class ArrayObjectFilter implements TypeFilter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var DeepCopy
|
||||||
|
*/
|
||||||
|
private $copier;
|
||||||
|
|
||||||
|
public function __construct(DeepCopy $copier)
|
||||||
|
{
|
||||||
|
$this->copier = $copier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function apply($arrayObject)
|
||||||
|
{
|
||||||
|
$clone = clone $arrayObject;
|
||||||
|
foreach ($arrayObject->getArrayCopy() as $k => $v) {
|
||||||
|
$clone->offsetSet($k, $this->copier->copy($v));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $clone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\TypeFilter\Spl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@see SplDoublyLinkedListFilter} instead.
|
||||||
|
*/
|
||||||
|
class SplDoublyLinkedList extends SplDoublyLinkedListFilter
|
||||||
|
{
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\TypeFilter\Spl;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use DeepCopy\DeepCopy;
|
||||||
|
use DeepCopy\TypeFilter\TypeFilter;
|
||||||
|
use SplDoublyLinkedList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @final
|
||||||
|
*/
|
||||||
|
class SplDoublyLinkedListFilter implements TypeFilter
|
||||||
|
{
|
||||||
|
private $copier;
|
||||||
|
|
||||||
|
public function __construct(DeepCopy $copier)
|
||||||
|
{
|
||||||
|
$this->copier = $copier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function apply($element)
|
||||||
|
{
|
||||||
|
$newElement = clone $element;
|
||||||
|
|
||||||
|
$copy = $this->createCopyClosure();
|
||||||
|
|
||||||
|
return $copy($newElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createCopyClosure()
|
||||||
|
{
|
||||||
|
$copier = $this->copier;
|
||||||
|
|
||||||
|
$copy = function (SplDoublyLinkedList $list) use ($copier) {
|
||||||
|
// Replace each element in the list with a deep copy of itself
|
||||||
|
for ($i = 1; $i <= $list->count(); $i++) {
|
||||||
|
$copy = $copier->recursiveCopy($list->shift());
|
||||||
|
|
||||||
|
$list->push($copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $list;
|
||||||
|
};
|
||||||
|
|
||||||
|
return Closure::bind($copy, null, DeepCopy::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\TypeFilter;
|
||||||
|
|
||||||
|
interface TypeFilter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Applies the filter to the object.
|
||||||
|
*
|
||||||
|
* @param mixed $element
|
||||||
|
*/
|
||||||
|
public function apply($element);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy\TypeMatcher;
|
||||||
|
|
||||||
|
class TypeMatcher
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $type
|
||||||
|
*/
|
||||||
|
public function __construct($type)
|
||||||
|
{
|
||||||
|
$this->type = $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $element
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function matches($element)
|
||||||
|
{
|
||||||
|
return is_object($element) ? is_a($element, $this->type) : gettype($element) === $this->type;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DeepCopy;
|
||||||
|
|
||||||
|
use function function_exists;
|
||||||
|
|
||||||
|
if (false === function_exists('DeepCopy\deep_copy')) {
|
||||||
|
/**
|
||||||
|
* Deep copies the given value.
|
||||||
|
*
|
||||||
|
* @param mixed $value
|
||||||
|
* @param bool $useCloneMethod
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
function deep_copy($value, $useCloneMethod = false)
|
||||||
|
{
|
||||||
|
return (new DeepCopy($useCloneMethod))->copy($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+29
@@ -0,0 +1,29 @@
|
|||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2011, Nikita Popov
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
Vendored
+233
@@ -0,0 +1,233 @@
|
|||||||
|
PHP Parser
|
||||||
|
==========
|
||||||
|
|
||||||
|
[](https://coveralls.io/github/nikic/PHP-Parser?branch=master)
|
||||||
|
|
||||||
|
This is a PHP parser written in PHP. Its purpose is to simplify static code analysis and
|
||||||
|
manipulation.
|
||||||
|
|
||||||
|
[**Documentation for version 5.x**][doc_master] (current; for running on PHP >= 7.4; for parsing PHP 7.0 to PHP 8.4, with limited support for parsing PHP 5.x).
|
||||||
|
|
||||||
|
[Documentation for version 4.x][doc_4_x] (supported; for running on PHP >= 7.0; for parsing PHP 5.2 to PHP 8.3).
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
The main features provided by this library are:
|
||||||
|
|
||||||
|
* Parsing PHP 7, and PHP 8 code into an abstract syntax tree (AST).
|
||||||
|
* Invalid code can be parsed into a partial AST.
|
||||||
|
* The AST contains accurate location information.
|
||||||
|
* Dumping the AST in human-readable form.
|
||||||
|
* Converting an AST back to PHP code.
|
||||||
|
* Formatting can be preserved for partially changed ASTs.
|
||||||
|
* Infrastructure to traverse and modify ASTs.
|
||||||
|
* Resolution of namespaced names.
|
||||||
|
* Evaluation of constant expressions.
|
||||||
|
* Builders to simplify AST construction for code generation.
|
||||||
|
* Converting an AST into JSON and back.
|
||||||
|
|
||||||
|
Quick Start
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Install the library using [composer](https://getcomposer.org):
|
||||||
|
|
||||||
|
php composer.phar require nikic/php-parser
|
||||||
|
|
||||||
|
Parse some PHP code into an AST and dump the result in human-readable form:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
use PhpParser\Error;
|
||||||
|
use PhpParser\NodeDumper;
|
||||||
|
use PhpParser\ParserFactory;
|
||||||
|
|
||||||
|
$code = <<<'CODE'
|
||||||
|
<?php
|
||||||
|
|
||||||
|
function test($foo)
|
||||||
|
{
|
||||||
|
var_dump($foo);
|
||||||
|
}
|
||||||
|
CODE;
|
||||||
|
|
||||||
|
$parser = (new ParserFactory())->createForNewestSupportedVersion();
|
||||||
|
try {
|
||||||
|
$ast = $parser->parse($code);
|
||||||
|
} catch (Error $error) {
|
||||||
|
echo "Parse error: {$error->getMessage()}\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dumper = new NodeDumper;
|
||||||
|
echo $dumper->dump($ast) . "\n";
|
||||||
|
```
|
||||||
|
|
||||||
|
This dumps an AST looking something like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
array(
|
||||||
|
0: Stmt_Function(
|
||||||
|
attrGroups: array(
|
||||||
|
)
|
||||||
|
byRef: false
|
||||||
|
name: Identifier(
|
||||||
|
name: test
|
||||||
|
)
|
||||||
|
params: array(
|
||||||
|
0: Param(
|
||||||
|
attrGroups: array(
|
||||||
|
)
|
||||||
|
flags: 0
|
||||||
|
type: null
|
||||||
|
byRef: false
|
||||||
|
variadic: false
|
||||||
|
var: Expr_Variable(
|
||||||
|
name: foo
|
||||||
|
)
|
||||||
|
default: null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
returnType: null
|
||||||
|
stmts: array(
|
||||||
|
0: Stmt_Expression(
|
||||||
|
expr: Expr_FuncCall(
|
||||||
|
name: Name(
|
||||||
|
name: var_dump
|
||||||
|
)
|
||||||
|
args: array(
|
||||||
|
0: Arg(
|
||||||
|
name: null
|
||||||
|
value: Expr_Variable(
|
||||||
|
name: foo
|
||||||
|
)
|
||||||
|
byRef: false
|
||||||
|
unpack: false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's traverse the AST and perform some kind of modification. For example, drop all function bodies:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Stmt\Function_;
|
||||||
|
use PhpParser\NodeTraverser;
|
||||||
|
use PhpParser\NodeVisitorAbstract;
|
||||||
|
|
||||||
|
$traverser = new NodeTraverser();
|
||||||
|
$traverser->addVisitor(new class extends NodeVisitorAbstract {
|
||||||
|
public function enterNode(Node $node) {
|
||||||
|
if ($node instanceof Function_) {
|
||||||
|
// Clean out the function body
|
||||||
|
$node->stmts = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$ast = $traverser->traverse($ast);
|
||||||
|
echo $dumper->dump($ast) . "\n";
|
||||||
|
```
|
||||||
|
|
||||||
|
This gives us an AST where the `Function_::$stmts` are empty:
|
||||||
|
|
||||||
|
```
|
||||||
|
array(
|
||||||
|
0: Stmt_Function(
|
||||||
|
attrGroups: array(
|
||||||
|
)
|
||||||
|
byRef: false
|
||||||
|
name: Identifier(
|
||||||
|
name: test
|
||||||
|
)
|
||||||
|
params: array(
|
||||||
|
0: Param(
|
||||||
|
attrGroups: array(
|
||||||
|
)
|
||||||
|
type: null
|
||||||
|
byRef: false
|
||||||
|
variadic: false
|
||||||
|
var: Expr_Variable(
|
||||||
|
name: foo
|
||||||
|
)
|
||||||
|
default: null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
returnType: null
|
||||||
|
stmts: array(
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, we can convert the new AST back to PHP code:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use PhpParser\PrettyPrinter;
|
||||||
|
|
||||||
|
$prettyPrinter = new PrettyPrinter\Standard;
|
||||||
|
echo $prettyPrinter->prettyPrintFile($ast);
|
||||||
|
```
|
||||||
|
|
||||||
|
This gives us our original code, minus the `var_dump()` call inside the function:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
function test($foo)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For a more comprehensive introduction, see the documentation.
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
1. [Introduction](doc/0_Introduction.markdown)
|
||||||
|
2. [Usage of basic components](doc/2_Usage_of_basic_components.markdown)
|
||||||
|
|
||||||
|
Component documentation:
|
||||||
|
|
||||||
|
* [Walking the AST](doc/component/Walking_the_AST.markdown)
|
||||||
|
* Node visitors
|
||||||
|
* Modifying the AST from a visitor
|
||||||
|
* Short-circuiting traversals
|
||||||
|
* Interleaved visitors
|
||||||
|
* Simple node finding API
|
||||||
|
* Parent and sibling references
|
||||||
|
* [Name resolution](doc/component/Name_resolution.markdown)
|
||||||
|
* Name resolver options
|
||||||
|
* Name resolution context
|
||||||
|
* [Pretty printing](doc/component/Pretty_printing.markdown)
|
||||||
|
* Converting AST back to PHP code
|
||||||
|
* Customizing formatting
|
||||||
|
* Formatting-preserving code transformations
|
||||||
|
* [AST builders](doc/component/AST_builders.markdown)
|
||||||
|
* Fluent builders for AST nodes
|
||||||
|
* [Lexer](doc/component/Lexer.markdown)
|
||||||
|
* Emulation
|
||||||
|
* Tokens, positions and attributes
|
||||||
|
* [Error handling](doc/component/Error_handling.markdown)
|
||||||
|
* Column information for errors
|
||||||
|
* Error recovery (parsing of syntactically incorrect code)
|
||||||
|
* [Constant expression evaluation](doc/component/Constant_expression_evaluation.markdown)
|
||||||
|
* Evaluating constant/property/etc initializers
|
||||||
|
* Handling errors and unsupported expressions
|
||||||
|
* [JSON representation](doc/component/JSON_representation.markdown)
|
||||||
|
* JSON encoding and decoding of ASTs
|
||||||
|
* [Performance](doc/component/Performance.markdown)
|
||||||
|
* Disabling Xdebug
|
||||||
|
* Reusing objects
|
||||||
|
* Garbage collection impact
|
||||||
|
* [Frequently asked questions](doc/component/FAQ.markdown)
|
||||||
|
* Parent and sibling references
|
||||||
|
|
||||||
|
[doc_3_x]: https://github.com/nikic/PHP-Parser/tree/3.x/doc
|
||||||
|
[doc_4_x]: https://github.com/nikic/PHP-Parser/tree/4.x/doc
|
||||||
|
[doc_master]: https://github.com/nikic/PHP-Parser/tree/master/doc
|
||||||
+206
@@ -0,0 +1,206 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../vendor/autoload.php'] as $file) {
|
||||||
|
if (file_exists($file)) {
|
||||||
|
require $file;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ini_set('xdebug.max_nesting_level', 3000);
|
||||||
|
|
||||||
|
// Disable Xdebug var_dump() output truncation
|
||||||
|
ini_set('xdebug.var_display_max_children', -1);
|
||||||
|
ini_set('xdebug.var_display_max_data', -1);
|
||||||
|
ini_set('xdebug.var_display_max_depth', -1);
|
||||||
|
|
||||||
|
list($operations, $files, $attributes) = parseArgs($argv);
|
||||||
|
|
||||||
|
/* Dump nodes by default */
|
||||||
|
if (empty($operations)) {
|
||||||
|
$operations[] = 'dump';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($files)) {
|
||||||
|
showHelp("Must specify at least one file.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$parser = (new PhpParser\ParserFactory())->createForVersion($attributes['version']);
|
||||||
|
$dumper = new PhpParser\NodeDumper([
|
||||||
|
'dumpComments' => true,
|
||||||
|
'dumpPositions' => $attributes['with-positions'],
|
||||||
|
]);
|
||||||
|
$prettyPrinter = new PhpParser\PrettyPrinter\Standard;
|
||||||
|
|
||||||
|
$traverser = new PhpParser\NodeTraverser();
|
||||||
|
$traverser->addVisitor(new PhpParser\NodeVisitor\NameResolver);
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if ($file === '-') {
|
||||||
|
$code = file_get_contents('php://stdin');
|
||||||
|
fwrite(STDERR, "====> Stdin:\n");
|
||||||
|
} else if (strpos($file, '<?php') === 0) {
|
||||||
|
$code = $file;
|
||||||
|
fwrite(STDERR, "====> Code $code\n");
|
||||||
|
} else {
|
||||||
|
if (!file_exists($file)) {
|
||||||
|
fwrite(STDERR, "File $file does not exist.\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = file_get_contents($file);
|
||||||
|
fwrite(STDERR, "====> File $file:\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($attributes['with-recovery']) {
|
||||||
|
$errorHandler = new PhpParser\ErrorHandler\Collecting;
|
||||||
|
$stmts = $parser->parse($code, $errorHandler);
|
||||||
|
foreach ($errorHandler->getErrors() as $error) {
|
||||||
|
$message = formatErrorMessage($error, $code, $attributes['with-column-info']);
|
||||||
|
fwrite(STDERR, $message . "\n");
|
||||||
|
}
|
||||||
|
if (null === $stmts) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$stmts = $parser->parse($code);
|
||||||
|
} catch (PhpParser\Error $error) {
|
||||||
|
$message = formatErrorMessage($error, $code, $attributes['with-column-info']);
|
||||||
|
fwrite(STDERR, $message . "\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($operations as $operation) {
|
||||||
|
if ('dump' === $operation) {
|
||||||
|
fwrite(STDERR, "==> Node dump:\n");
|
||||||
|
echo $dumper->dump($stmts, $code), "\n";
|
||||||
|
} elseif ('pretty-print' === $operation) {
|
||||||
|
fwrite(STDERR, "==> Pretty print:\n");
|
||||||
|
echo $prettyPrinter->prettyPrintFile($stmts), "\n";
|
||||||
|
} elseif ('json-dump' === $operation) {
|
||||||
|
fwrite(STDERR, "==> JSON dump:\n");
|
||||||
|
echo json_encode($stmts, JSON_PRETTY_PRINT), "\n";
|
||||||
|
} elseif ('var-dump' === $operation) {
|
||||||
|
fwrite(STDERR, "==> var_dump():\n");
|
||||||
|
var_dump($stmts);
|
||||||
|
} elseif ('resolve-names' === $operation) {
|
||||||
|
fwrite(STDERR, "==> Resolved names.\n");
|
||||||
|
$stmts = $traverser->traverse($stmts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatErrorMessage(PhpParser\Error $e, $code, $withColumnInfo) {
|
||||||
|
if ($withColumnInfo && $e->hasColumnInfo()) {
|
||||||
|
return $e->getMessageWithColumnInfo($code);
|
||||||
|
} else {
|
||||||
|
return $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHelp($error = '') {
|
||||||
|
if ($error) {
|
||||||
|
fwrite(STDERR, $error . "\n\n");
|
||||||
|
}
|
||||||
|
fwrite($error ? STDERR : STDOUT, <<<'OUTPUT'
|
||||||
|
Usage: php-parse [operations] file1.php [file2.php ...]
|
||||||
|
or: php-parse [operations] "<?php code"
|
||||||
|
Turn PHP source code into an abstract syntax tree.
|
||||||
|
|
||||||
|
Operations is a list of the following options (--dump by default):
|
||||||
|
|
||||||
|
-d, --dump Dump nodes using NodeDumper
|
||||||
|
-p, --pretty-print Pretty print file using PrettyPrinter\Standard
|
||||||
|
-j, --json-dump Print json_encode() result
|
||||||
|
--var-dump var_dump() nodes (for exact structure)
|
||||||
|
-N, --resolve-names Resolve names using NodeVisitor\NameResolver
|
||||||
|
-c, --with-column-info Show column-numbers for errors (if available)
|
||||||
|
-P, --with-positions Show positions in node dumps
|
||||||
|
-r, --with-recovery Use parsing with error recovery
|
||||||
|
--version=VERSION Target specific PHP version (default: newest)
|
||||||
|
-h, --help Display this page
|
||||||
|
|
||||||
|
Example:
|
||||||
|
php-parse -d -p -N -d file.php
|
||||||
|
|
||||||
|
Dumps nodes, pretty prints them, then resolves names and dumps them again.
|
||||||
|
|
||||||
|
|
||||||
|
OUTPUT
|
||||||
|
);
|
||||||
|
exit($error ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs($args) {
|
||||||
|
$operations = [];
|
||||||
|
$files = [];
|
||||||
|
$attributes = [
|
||||||
|
'with-column-info' => false,
|
||||||
|
'with-positions' => false,
|
||||||
|
'with-recovery' => false,
|
||||||
|
'version' => PhpParser\PhpVersion::getNewestSupported(),
|
||||||
|
];
|
||||||
|
|
||||||
|
array_shift($args);
|
||||||
|
$parseOptions = true;
|
||||||
|
foreach ($args as $arg) {
|
||||||
|
if (!$parseOptions) {
|
||||||
|
$files[] = $arg;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($arg) {
|
||||||
|
case '--dump':
|
||||||
|
case '-d':
|
||||||
|
$operations[] = 'dump';
|
||||||
|
break;
|
||||||
|
case '--pretty-print':
|
||||||
|
case '-p':
|
||||||
|
$operations[] = 'pretty-print';
|
||||||
|
break;
|
||||||
|
case '--json-dump':
|
||||||
|
case '-j':
|
||||||
|
$operations[] = 'json-dump';
|
||||||
|
break;
|
||||||
|
case '--var-dump':
|
||||||
|
$operations[] = 'var-dump';
|
||||||
|
break;
|
||||||
|
case '--resolve-names':
|
||||||
|
case '-N':
|
||||||
|
$operations[] = 'resolve-names';
|
||||||
|
break;
|
||||||
|
case '--with-column-info':
|
||||||
|
case '-c':
|
||||||
|
$attributes['with-column-info'] = true;
|
||||||
|
break;
|
||||||
|
case '--with-positions':
|
||||||
|
case '-P':
|
||||||
|
$attributes['with-positions'] = true;
|
||||||
|
break;
|
||||||
|
case '--with-recovery':
|
||||||
|
case '-r':
|
||||||
|
$attributes['with-recovery'] = true;
|
||||||
|
break;
|
||||||
|
case '--help':
|
||||||
|
case '-h':
|
||||||
|
showHelp();
|
||||||
|
break;
|
||||||
|
case '--':
|
||||||
|
$parseOptions = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (preg_match('/^--version=(.*)$/', $arg, $matches)) {
|
||||||
|
$attributes['version'] = PhpParser\PhpVersion::fromString($matches[1]);
|
||||||
|
} elseif ($arg[0] === '-' && \strlen($arg[0]) > 1) {
|
||||||
|
showHelp("Invalid operation $arg.");
|
||||||
|
} else {
|
||||||
|
$files[] = $arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$operations, $files, $attributes];
|
||||||
|
}
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "nikic/php-parser",
|
||||||
|
"type": "library",
|
||||||
|
"description": "A PHP parser written in PHP",
|
||||||
|
"keywords": [
|
||||||
|
"php",
|
||||||
|
"parser"
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Nikita Popov"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.4",
|
||||||
|
"ext-tokenizer": "*",
|
||||||
|
"ext-json": "*",
|
||||||
|
"ext-ctype": "*"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^9.0",
|
||||||
|
"ircmaxell/php-yacc": "^0.0.7"
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "5.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"PhpParser\\": "lib/PhpParser"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"PhpParser\\": "test/PhpParser/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bin": [
|
||||||
|
"bin/php-parse"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser;
|
||||||
|
|
||||||
|
interface Builder {
|
||||||
|
/**
|
||||||
|
* Returns the built node.
|
||||||
|
*
|
||||||
|
* @return Node The built node
|
||||||
|
*/
|
||||||
|
public function getNode(): Node;
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Builder;
|
||||||
|
|
||||||
|
use PhpParser;
|
||||||
|
use PhpParser\BuilderHelpers;
|
||||||
|
use PhpParser\Modifiers;
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Const_;
|
||||||
|
use PhpParser\Node\Identifier;
|
||||||
|
use PhpParser\Node\Stmt;
|
||||||
|
|
||||||
|
class ClassConst implements PhpParser\Builder {
|
||||||
|
protected int $flags = 0;
|
||||||
|
/** @var array<string, mixed> */
|
||||||
|
protected array $attributes = [];
|
||||||
|
/** @var list<Const_> */
|
||||||
|
protected array $constants = [];
|
||||||
|
|
||||||
|
/** @var list<Node\AttributeGroup> */
|
||||||
|
protected array $attributeGroups = [];
|
||||||
|
/** @var Identifier|Node\Name|Node\ComplexType|null */
|
||||||
|
protected ?Node $type = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a class constant builder
|
||||||
|
*
|
||||||
|
* @param string|Identifier $name Name
|
||||||
|
* @param Node\Expr|bool|null|int|float|string|array|\UnitEnum $value Value
|
||||||
|
*/
|
||||||
|
public function __construct($name, $value) {
|
||||||
|
$this->constants = [new Const_($name, BuilderHelpers::normalizeValue($value))];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add another constant to const group
|
||||||
|
*
|
||||||
|
* @param string|Identifier $name Name
|
||||||
|
* @param Node\Expr|bool|null|int|float|string|array|\UnitEnum $value Value
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addConst($name, $value) {
|
||||||
|
$this->constants[] = new Const_($name, BuilderHelpers::normalizeValue($value));
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the constant public.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makePublic() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::PUBLIC);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the constant protected.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeProtected() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::PROTECTED);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the constant private.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makePrivate() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::PRIVATE);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the constant final.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeFinal() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::FINAL);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets doc comment for the constant.
|
||||||
|
*
|
||||||
|
* @param PhpParser\Comment\Doc|string $docComment Doc comment to set
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function setDocComment($docComment) {
|
||||||
|
$this->attributes = [
|
||||||
|
'comments' => [BuilderHelpers::normalizeDocComment($docComment)]
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an attribute group.
|
||||||
|
*
|
||||||
|
* @param Node\Attribute|Node\AttributeGroup $attribute
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addAttribute($attribute) {
|
||||||
|
$this->attributeGroups[] = BuilderHelpers::normalizeAttribute($attribute);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the constant type.
|
||||||
|
*
|
||||||
|
* @param string|Node\Name|Identifier|Node\ComplexType $type
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setType($type) {
|
||||||
|
$this->type = BuilderHelpers::normalizeType($type);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the built class node.
|
||||||
|
*
|
||||||
|
* @return Stmt\ClassConst The built constant node
|
||||||
|
*/
|
||||||
|
public function getNode(): PhpParser\Node {
|
||||||
|
return new Stmt\ClassConst(
|
||||||
|
$this->constants,
|
||||||
|
$this->flags,
|
||||||
|
$this->attributes,
|
||||||
|
$this->attributeGroups,
|
||||||
|
$this->type
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Builder;
|
||||||
|
|
||||||
|
use PhpParser;
|
||||||
|
use PhpParser\BuilderHelpers;
|
||||||
|
use PhpParser\Modifiers;
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Name;
|
||||||
|
use PhpParser\Node\Stmt;
|
||||||
|
|
||||||
|
class Class_ extends Declaration {
|
||||||
|
protected string $name;
|
||||||
|
protected ?Name $extends = null;
|
||||||
|
/** @var list<Name> */
|
||||||
|
protected array $implements = [];
|
||||||
|
protected int $flags = 0;
|
||||||
|
/** @var list<Stmt\TraitUse> */
|
||||||
|
protected array $uses = [];
|
||||||
|
/** @var list<Stmt\ClassConst> */
|
||||||
|
protected array $constants = [];
|
||||||
|
/** @var list<Stmt\Property> */
|
||||||
|
protected array $properties = [];
|
||||||
|
/** @var list<Stmt\ClassMethod> */
|
||||||
|
protected array $methods = [];
|
||||||
|
/** @var list<Node\AttributeGroup> */
|
||||||
|
protected array $attributeGroups = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a class builder.
|
||||||
|
*
|
||||||
|
* @param string $name Name of the class
|
||||||
|
*/
|
||||||
|
public function __construct(string $name) {
|
||||||
|
$this->name = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends a class.
|
||||||
|
*
|
||||||
|
* @param Name|string $class Name of class to extend
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function extend($class) {
|
||||||
|
$this->extends = BuilderHelpers::normalizeName($class);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements one or more interfaces.
|
||||||
|
*
|
||||||
|
* @param Name|string ...$interfaces Names of interfaces to implement
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function implement(...$interfaces) {
|
||||||
|
foreach ($interfaces as $interface) {
|
||||||
|
$this->implements[] = BuilderHelpers::normalizeName($interface);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the class abstract.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeAbstract() {
|
||||||
|
$this->flags = BuilderHelpers::addClassModifier($this->flags, Modifiers::ABSTRACT);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the class final.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeFinal() {
|
||||||
|
$this->flags = BuilderHelpers::addClassModifier($this->flags, Modifiers::FINAL);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the class readonly.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeReadonly() {
|
||||||
|
$this->flags = BuilderHelpers::addClassModifier($this->flags, Modifiers::READONLY);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a statement.
|
||||||
|
*
|
||||||
|
* @param Stmt|PhpParser\Builder $stmt The statement to add
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addStmt($stmt) {
|
||||||
|
$stmt = BuilderHelpers::normalizeNode($stmt);
|
||||||
|
|
||||||
|
if ($stmt instanceof Stmt\Property) {
|
||||||
|
$this->properties[] = $stmt;
|
||||||
|
} elseif ($stmt instanceof Stmt\ClassMethod) {
|
||||||
|
$this->methods[] = $stmt;
|
||||||
|
} elseif ($stmt instanceof Stmt\TraitUse) {
|
||||||
|
$this->uses[] = $stmt;
|
||||||
|
} elseif ($stmt instanceof Stmt\ClassConst) {
|
||||||
|
$this->constants[] = $stmt;
|
||||||
|
} else {
|
||||||
|
throw new \LogicException(sprintf('Unexpected node of type "%s"', $stmt->getType()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an attribute group.
|
||||||
|
*
|
||||||
|
* @param Node\Attribute|Node\AttributeGroup $attribute
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addAttribute($attribute) {
|
||||||
|
$this->attributeGroups[] = BuilderHelpers::normalizeAttribute($attribute);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the built class node.
|
||||||
|
*
|
||||||
|
* @return Stmt\Class_ The built class node
|
||||||
|
*/
|
||||||
|
public function getNode(): PhpParser\Node {
|
||||||
|
return new Stmt\Class_($this->name, [
|
||||||
|
'flags' => $this->flags,
|
||||||
|
'extends' => $this->extends,
|
||||||
|
'implements' => $this->implements,
|
||||||
|
'stmts' => array_merge($this->uses, $this->constants, $this->properties, $this->methods),
|
||||||
|
'attrGroups' => $this->attributeGroups,
|
||||||
|
], $this->attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Builder;
|
||||||
|
|
||||||
|
use PhpParser;
|
||||||
|
use PhpParser\BuilderHelpers;
|
||||||
|
|
||||||
|
abstract class Declaration implements PhpParser\Builder {
|
||||||
|
/** @var array<string, mixed> */
|
||||||
|
protected array $attributes = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a statement.
|
||||||
|
*
|
||||||
|
* @param PhpParser\Node\Stmt|PhpParser\Builder $stmt The statement to add
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
abstract public function addStmt($stmt);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds multiple statements.
|
||||||
|
*
|
||||||
|
* @param (PhpParser\Node\Stmt|PhpParser\Builder)[] $stmts The statements to add
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addStmts(array $stmts) {
|
||||||
|
foreach ($stmts as $stmt) {
|
||||||
|
$this->addStmt($stmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets doc comment for the declaration.
|
||||||
|
*
|
||||||
|
* @param PhpParser\Comment\Doc|string $docComment Doc comment to set
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function setDocComment($docComment) {
|
||||||
|
$this->attributes['comments'] = [
|
||||||
|
BuilderHelpers::normalizeDocComment($docComment)
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Builder;
|
||||||
|
|
||||||
|
use PhpParser;
|
||||||
|
use PhpParser\BuilderHelpers;
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Identifier;
|
||||||
|
use PhpParser\Node\Stmt;
|
||||||
|
|
||||||
|
class EnumCase implements PhpParser\Builder {
|
||||||
|
/** @var Identifier|string */
|
||||||
|
protected $name;
|
||||||
|
protected ?Node\Expr $value = null;
|
||||||
|
/** @var array<string, mixed> */
|
||||||
|
protected array $attributes = [];
|
||||||
|
|
||||||
|
/** @var list<Node\AttributeGroup> */
|
||||||
|
protected array $attributeGroups = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an enum case builder.
|
||||||
|
*
|
||||||
|
* @param string|Identifier $name Name
|
||||||
|
*/
|
||||||
|
public function __construct($name) {
|
||||||
|
$this->name = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the value.
|
||||||
|
*
|
||||||
|
* @param Node\Expr|string|int $value
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setValue($value) {
|
||||||
|
$this->value = BuilderHelpers::normalizeValue($value);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets doc comment for the constant.
|
||||||
|
*
|
||||||
|
* @param PhpParser\Comment\Doc|string $docComment Doc comment to set
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function setDocComment($docComment) {
|
||||||
|
$this->attributes = [
|
||||||
|
'comments' => [BuilderHelpers::normalizeDocComment($docComment)]
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an attribute group.
|
||||||
|
*
|
||||||
|
* @param Node\Attribute|Node\AttributeGroup $attribute
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addAttribute($attribute) {
|
||||||
|
$this->attributeGroups[] = BuilderHelpers::normalizeAttribute($attribute);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the built enum case node.
|
||||||
|
*
|
||||||
|
* @return Stmt\EnumCase The built constant node
|
||||||
|
*/
|
||||||
|
public function getNode(): PhpParser\Node {
|
||||||
|
return new Stmt\EnumCase(
|
||||||
|
$this->name,
|
||||||
|
$this->value,
|
||||||
|
$this->attributeGroups,
|
||||||
|
$this->attributes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Builder;
|
||||||
|
|
||||||
|
use PhpParser;
|
||||||
|
use PhpParser\BuilderHelpers;
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Identifier;
|
||||||
|
use PhpParser\Node\Name;
|
||||||
|
use PhpParser\Node\Stmt;
|
||||||
|
|
||||||
|
class Enum_ extends Declaration {
|
||||||
|
protected string $name;
|
||||||
|
protected ?Identifier $scalarType = null;
|
||||||
|
/** @var list<Name> */
|
||||||
|
protected array $implements = [];
|
||||||
|
/** @var list<Stmt\TraitUse> */
|
||||||
|
protected array $uses = [];
|
||||||
|
/** @var list<Stmt\EnumCase> */
|
||||||
|
protected array $enumCases = [];
|
||||||
|
/** @var list<Stmt\ClassConst> */
|
||||||
|
protected array $constants = [];
|
||||||
|
/** @var list<Stmt\ClassMethod> */
|
||||||
|
protected array $methods = [];
|
||||||
|
/** @var list<Node\AttributeGroup> */
|
||||||
|
protected array $attributeGroups = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an enum builder.
|
||||||
|
*
|
||||||
|
* @param string $name Name of the enum
|
||||||
|
*/
|
||||||
|
public function __construct(string $name) {
|
||||||
|
$this->name = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the scalar type.
|
||||||
|
*
|
||||||
|
* @param string|Identifier $scalarType
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setScalarType($scalarType) {
|
||||||
|
$this->scalarType = BuilderHelpers::normalizeType($scalarType);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements one or more interfaces.
|
||||||
|
*
|
||||||
|
* @param Name|string ...$interfaces Names of interfaces to implement
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function implement(...$interfaces) {
|
||||||
|
foreach ($interfaces as $interface) {
|
||||||
|
$this->implements[] = BuilderHelpers::normalizeName($interface);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a statement.
|
||||||
|
*
|
||||||
|
* @param Stmt|PhpParser\Builder $stmt The statement to add
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addStmt($stmt) {
|
||||||
|
$stmt = BuilderHelpers::normalizeNode($stmt);
|
||||||
|
|
||||||
|
if ($stmt instanceof Stmt\EnumCase) {
|
||||||
|
$this->enumCases[] = $stmt;
|
||||||
|
} elseif ($stmt instanceof Stmt\ClassMethod) {
|
||||||
|
$this->methods[] = $stmt;
|
||||||
|
} elseif ($stmt instanceof Stmt\TraitUse) {
|
||||||
|
$this->uses[] = $stmt;
|
||||||
|
} elseif ($stmt instanceof Stmt\ClassConst) {
|
||||||
|
$this->constants[] = $stmt;
|
||||||
|
} else {
|
||||||
|
throw new \LogicException(sprintf('Unexpected node of type "%s"', $stmt->getType()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an attribute group.
|
||||||
|
*
|
||||||
|
* @param Node\Attribute|Node\AttributeGroup $attribute
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addAttribute($attribute) {
|
||||||
|
$this->attributeGroups[] = BuilderHelpers::normalizeAttribute($attribute);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the built class node.
|
||||||
|
*
|
||||||
|
* @return Stmt\Enum_ The built enum node
|
||||||
|
*/
|
||||||
|
public function getNode(): PhpParser\Node {
|
||||||
|
return new Stmt\Enum_($this->name, [
|
||||||
|
'scalarType' => $this->scalarType,
|
||||||
|
'implements' => $this->implements,
|
||||||
|
'stmts' => array_merge($this->uses, $this->enumCases, $this->constants, $this->methods),
|
||||||
|
'attrGroups' => $this->attributeGroups,
|
||||||
|
], $this->attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Builder;
|
||||||
|
|
||||||
|
use PhpParser\BuilderHelpers;
|
||||||
|
use PhpParser\Node;
|
||||||
|
|
||||||
|
abstract class FunctionLike extends Declaration {
|
||||||
|
protected bool $returnByRef = false;
|
||||||
|
/** @var Node\Param[] */
|
||||||
|
protected array $params = [];
|
||||||
|
|
||||||
|
/** @var Node\Identifier|Node\Name|Node\ComplexType|null */
|
||||||
|
protected ?Node $returnType = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make the function return by reference.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeReturnByRef() {
|
||||||
|
$this->returnByRef = true;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a parameter.
|
||||||
|
*
|
||||||
|
* @param Node\Param|Param $param The parameter to add
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addParam($param) {
|
||||||
|
$param = BuilderHelpers::normalizeNode($param);
|
||||||
|
|
||||||
|
if (!$param instanceof Node\Param) {
|
||||||
|
throw new \LogicException(sprintf('Expected parameter node, got "%s"', $param->getType()));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->params[] = $param;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds multiple parameters.
|
||||||
|
*
|
||||||
|
* @param (Node\Param|Param)[] $params The parameters to add
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addParams(array $params) {
|
||||||
|
foreach ($params as $param) {
|
||||||
|
$this->addParam($param);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the return type for PHP 7.
|
||||||
|
*
|
||||||
|
* @param string|Node\Name|Node\Identifier|Node\ComplexType $type
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function setReturnType($type) {
|
||||||
|
$this->returnType = BuilderHelpers::normalizeType($type);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Builder;
|
||||||
|
|
||||||
|
use PhpParser;
|
||||||
|
use PhpParser\BuilderHelpers;
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Stmt;
|
||||||
|
|
||||||
|
class Function_ extends FunctionLike {
|
||||||
|
protected string $name;
|
||||||
|
/** @var list<Stmt> */
|
||||||
|
protected array $stmts = [];
|
||||||
|
|
||||||
|
/** @var list<Node\AttributeGroup> */
|
||||||
|
protected array $attributeGroups = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a function builder.
|
||||||
|
*
|
||||||
|
* @param string $name Name of the function
|
||||||
|
*/
|
||||||
|
public function __construct(string $name) {
|
||||||
|
$this->name = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a statement.
|
||||||
|
*
|
||||||
|
* @param Node|PhpParser\Builder $stmt The statement to add
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addStmt($stmt) {
|
||||||
|
$this->stmts[] = BuilderHelpers::normalizeStmt($stmt);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an attribute group.
|
||||||
|
*
|
||||||
|
* @param Node\Attribute|Node\AttributeGroup $attribute
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addAttribute($attribute) {
|
||||||
|
$this->attributeGroups[] = BuilderHelpers::normalizeAttribute($attribute);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the built function node.
|
||||||
|
*
|
||||||
|
* @return Stmt\Function_ The built function node
|
||||||
|
*/
|
||||||
|
public function getNode(): Node {
|
||||||
|
return new Stmt\Function_($this->name, [
|
||||||
|
'byRef' => $this->returnByRef,
|
||||||
|
'params' => $this->params,
|
||||||
|
'returnType' => $this->returnType,
|
||||||
|
'stmts' => $this->stmts,
|
||||||
|
'attrGroups' => $this->attributeGroups,
|
||||||
|
], $this->attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Builder;
|
||||||
|
|
||||||
|
use PhpParser;
|
||||||
|
use PhpParser\BuilderHelpers;
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Name;
|
||||||
|
use PhpParser\Node\Stmt;
|
||||||
|
|
||||||
|
class Interface_ extends Declaration {
|
||||||
|
protected string $name;
|
||||||
|
/** @var list<Name> */
|
||||||
|
protected array $extends = [];
|
||||||
|
/** @var list<Stmt\ClassConst> */
|
||||||
|
protected array $constants = [];
|
||||||
|
/** @var list<Stmt\ClassMethod> */
|
||||||
|
protected array $methods = [];
|
||||||
|
/** @var list<Node\AttributeGroup> */
|
||||||
|
protected array $attributeGroups = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an interface builder.
|
||||||
|
*
|
||||||
|
* @param string $name Name of the interface
|
||||||
|
*/
|
||||||
|
public function __construct(string $name) {
|
||||||
|
$this->name = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends one or more interfaces.
|
||||||
|
*
|
||||||
|
* @param Name|string ...$interfaces Names of interfaces to extend
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function extend(...$interfaces) {
|
||||||
|
foreach ($interfaces as $interface) {
|
||||||
|
$this->extends[] = BuilderHelpers::normalizeName($interface);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a statement.
|
||||||
|
*
|
||||||
|
* @param Stmt|PhpParser\Builder $stmt The statement to add
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addStmt($stmt) {
|
||||||
|
$stmt = BuilderHelpers::normalizeNode($stmt);
|
||||||
|
|
||||||
|
if ($stmt instanceof Stmt\ClassConst) {
|
||||||
|
$this->constants[] = $stmt;
|
||||||
|
} elseif ($stmt instanceof Stmt\ClassMethod) {
|
||||||
|
// we erase all statements in the body of an interface method
|
||||||
|
$stmt->stmts = null;
|
||||||
|
$this->methods[] = $stmt;
|
||||||
|
} else {
|
||||||
|
throw new \LogicException(sprintf('Unexpected node of type "%s"', $stmt->getType()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an attribute group.
|
||||||
|
*
|
||||||
|
* @param Node\Attribute|Node\AttributeGroup $attribute
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addAttribute($attribute) {
|
||||||
|
$this->attributeGroups[] = BuilderHelpers::normalizeAttribute($attribute);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the built interface node.
|
||||||
|
*
|
||||||
|
* @return Stmt\Interface_ The built interface node
|
||||||
|
*/
|
||||||
|
public function getNode(): PhpParser\Node {
|
||||||
|
return new Stmt\Interface_($this->name, [
|
||||||
|
'extends' => $this->extends,
|
||||||
|
'stmts' => array_merge($this->constants, $this->methods),
|
||||||
|
'attrGroups' => $this->attributeGroups,
|
||||||
|
], $this->attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Builder;
|
||||||
|
|
||||||
|
use PhpParser;
|
||||||
|
use PhpParser\BuilderHelpers;
|
||||||
|
use PhpParser\Modifiers;
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Stmt;
|
||||||
|
|
||||||
|
class Method extends FunctionLike {
|
||||||
|
protected string $name;
|
||||||
|
|
||||||
|
protected int $flags = 0;
|
||||||
|
|
||||||
|
/** @var list<Stmt>|null */
|
||||||
|
protected ?array $stmts = [];
|
||||||
|
|
||||||
|
/** @var list<Node\AttributeGroup> */
|
||||||
|
protected array $attributeGroups = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a method builder.
|
||||||
|
*
|
||||||
|
* @param string $name Name of the method
|
||||||
|
*/
|
||||||
|
public function __construct(string $name) {
|
||||||
|
$this->name = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the method public.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makePublic() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::PUBLIC);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the method protected.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeProtected() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::PROTECTED);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the method private.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makePrivate() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::PRIVATE);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the method static.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeStatic() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::STATIC);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the method abstract.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeAbstract() {
|
||||||
|
if (!empty($this->stmts)) {
|
||||||
|
throw new \LogicException('Cannot make method with statements abstract');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::ABSTRACT);
|
||||||
|
$this->stmts = null; // abstract methods don't have statements
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the method final.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeFinal() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::FINAL);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a statement.
|
||||||
|
*
|
||||||
|
* @param Node|PhpParser\Builder $stmt The statement to add
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addStmt($stmt) {
|
||||||
|
if (null === $this->stmts) {
|
||||||
|
throw new \LogicException('Cannot add statements to an abstract method');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stmts[] = BuilderHelpers::normalizeStmt($stmt);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an attribute group.
|
||||||
|
*
|
||||||
|
* @param Node\Attribute|Node\AttributeGroup $attribute
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addAttribute($attribute) {
|
||||||
|
$this->attributeGroups[] = BuilderHelpers::normalizeAttribute($attribute);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the built method node.
|
||||||
|
*
|
||||||
|
* @return Stmt\ClassMethod The built method node
|
||||||
|
*/
|
||||||
|
public function getNode(): Node {
|
||||||
|
return new Stmt\ClassMethod($this->name, [
|
||||||
|
'flags' => $this->flags,
|
||||||
|
'byRef' => $this->returnByRef,
|
||||||
|
'params' => $this->params,
|
||||||
|
'returnType' => $this->returnType,
|
||||||
|
'stmts' => $this->stmts,
|
||||||
|
'attrGroups' => $this->attributeGroups,
|
||||||
|
], $this->attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Builder;
|
||||||
|
|
||||||
|
use PhpParser;
|
||||||
|
use PhpParser\BuilderHelpers;
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Stmt;
|
||||||
|
|
||||||
|
class Namespace_ extends Declaration {
|
||||||
|
private ?Node\Name $name;
|
||||||
|
/** @var Stmt[] */
|
||||||
|
private array $stmts = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a namespace builder.
|
||||||
|
*
|
||||||
|
* @param Node\Name|string|null $name Name of the namespace
|
||||||
|
*/
|
||||||
|
public function __construct($name) {
|
||||||
|
$this->name = null !== $name ? BuilderHelpers::normalizeName($name) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a statement.
|
||||||
|
*
|
||||||
|
* @param Node|PhpParser\Builder $stmt The statement to add
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addStmt($stmt) {
|
||||||
|
$this->stmts[] = BuilderHelpers::normalizeStmt($stmt);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the built node.
|
||||||
|
*
|
||||||
|
* @return Stmt\Namespace_ The built node
|
||||||
|
*/
|
||||||
|
public function getNode(): Node {
|
||||||
|
return new Stmt\Namespace_($this->name, $this->stmts, $this->attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Builder;
|
||||||
|
|
||||||
|
use PhpParser;
|
||||||
|
use PhpParser\BuilderHelpers;
|
||||||
|
use PhpParser\Modifiers;
|
||||||
|
use PhpParser\Node;
|
||||||
|
|
||||||
|
class Param implements PhpParser\Builder {
|
||||||
|
protected string $name;
|
||||||
|
protected ?Node\Expr $default = null;
|
||||||
|
/** @var Node\Identifier|Node\Name|Node\ComplexType|null */
|
||||||
|
protected ?Node $type = null;
|
||||||
|
protected bool $byRef = false;
|
||||||
|
protected int $flags = 0;
|
||||||
|
protected bool $variadic = false;
|
||||||
|
/** @var list<Node\AttributeGroup> */
|
||||||
|
protected array $attributeGroups = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a parameter builder.
|
||||||
|
*
|
||||||
|
* @param string $name Name of the parameter
|
||||||
|
*/
|
||||||
|
public function __construct(string $name) {
|
||||||
|
$this->name = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets default value for the parameter.
|
||||||
|
*
|
||||||
|
* @param mixed $value Default value to use
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function setDefault($value) {
|
||||||
|
$this->default = BuilderHelpers::normalizeValue($value);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets type for the parameter.
|
||||||
|
*
|
||||||
|
* @param string|Node\Name|Node\Identifier|Node\ComplexType $type Parameter type
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function setType($type) {
|
||||||
|
$this->type = BuilderHelpers::normalizeType($type);
|
||||||
|
if ($this->type == 'void') {
|
||||||
|
throw new \LogicException('Parameter type cannot be void');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make the parameter accept the value by reference.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeByRef() {
|
||||||
|
$this->byRef = true;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make the parameter variadic
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeVariadic() {
|
||||||
|
$this->variadic = true;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the (promoted) parameter public.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makePublic() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::PUBLIC);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the (promoted) parameter protected.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeProtected() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::PROTECTED);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the (promoted) parameter private.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makePrivate() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::PRIVATE);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the (promoted) parameter readonly.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeReadonly() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::READONLY);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gives the promoted property private(set) visibility.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makePrivateSet() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::PRIVATE_SET);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gives the promoted property protected(set) visibility.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeProtectedSet() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::PROTECTED_SET);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an attribute group.
|
||||||
|
*
|
||||||
|
* @param Node\Attribute|Node\AttributeGroup $attribute
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addAttribute($attribute) {
|
||||||
|
$this->attributeGroups[] = BuilderHelpers::normalizeAttribute($attribute);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the built parameter node.
|
||||||
|
*
|
||||||
|
* @return Node\Param The built parameter node
|
||||||
|
*/
|
||||||
|
public function getNode(): Node {
|
||||||
|
return new Node\Param(
|
||||||
|
new Node\Expr\Variable($this->name),
|
||||||
|
$this->default, $this->type, $this->byRef, $this->variadic, [], $this->flags, $this->attributeGroups
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Builder;
|
||||||
|
|
||||||
|
use PhpParser;
|
||||||
|
use PhpParser\BuilderHelpers;
|
||||||
|
use PhpParser\Modifiers;
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Identifier;
|
||||||
|
use PhpParser\Node\Name;
|
||||||
|
use PhpParser\Node\Stmt;
|
||||||
|
use PhpParser\Node\ComplexType;
|
||||||
|
|
||||||
|
class Property implements PhpParser\Builder {
|
||||||
|
protected string $name;
|
||||||
|
|
||||||
|
protected int $flags = 0;
|
||||||
|
|
||||||
|
protected ?Node\Expr $default = null;
|
||||||
|
/** @var array<string, mixed> */
|
||||||
|
protected array $attributes = [];
|
||||||
|
/** @var null|Identifier|Name|ComplexType */
|
||||||
|
protected ?Node $type = null;
|
||||||
|
/** @var list<Node\AttributeGroup> */
|
||||||
|
protected array $attributeGroups = [];
|
||||||
|
/** @var list<Node\PropertyHook> */
|
||||||
|
protected array $hooks = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a property builder.
|
||||||
|
*
|
||||||
|
* @param string $name Name of the property
|
||||||
|
*/
|
||||||
|
public function __construct(string $name) {
|
||||||
|
$this->name = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the property public.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makePublic() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::PUBLIC);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the property protected.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeProtected() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::PROTECTED);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the property private.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makePrivate() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::PRIVATE);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the property static.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeStatic() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::STATIC);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the property readonly.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeReadonly() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::READONLY);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the property abstract. Requires at least one property hook to be specified as well.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeAbstract() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::ABSTRACT);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the property final.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeFinal() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::FINAL);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gives the property private(set) visibility.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makePrivateSet() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::PRIVATE_SET);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gives the property protected(set) visibility.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeProtectedSet() {
|
||||||
|
$this->flags = BuilderHelpers::addModifier($this->flags, Modifiers::PROTECTED_SET);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets default value for the property.
|
||||||
|
*
|
||||||
|
* @param mixed $value Default value to use
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function setDefault($value) {
|
||||||
|
$this->default = BuilderHelpers::normalizeValue($value);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets doc comment for the property.
|
||||||
|
*
|
||||||
|
* @param PhpParser\Comment\Doc|string $docComment Doc comment to set
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function setDocComment($docComment) {
|
||||||
|
$this->attributes = [
|
||||||
|
'comments' => [BuilderHelpers::normalizeDocComment($docComment)]
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the property type for PHP 7.4+.
|
||||||
|
*
|
||||||
|
* @param string|Name|Identifier|ComplexType $type
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setType($type) {
|
||||||
|
$this->type = BuilderHelpers::normalizeType($type);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an attribute group.
|
||||||
|
*
|
||||||
|
* @param Node\Attribute|Node\AttributeGroup $attribute
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addAttribute($attribute) {
|
||||||
|
$this->attributeGroups[] = BuilderHelpers::normalizeAttribute($attribute);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a property hook.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addHook(Node\PropertyHook $hook) {
|
||||||
|
$this->hooks[] = $hook;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the built class node.
|
||||||
|
*
|
||||||
|
* @return Stmt\Property The built property node
|
||||||
|
*/
|
||||||
|
public function getNode(): PhpParser\Node {
|
||||||
|
if ($this->flags & Modifiers::ABSTRACT && !$this->hooks) {
|
||||||
|
throw new PhpParser\Error('Only hooked properties may be declared abstract');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Stmt\Property(
|
||||||
|
$this->flags !== 0 ? $this->flags : Modifiers::PUBLIC,
|
||||||
|
[
|
||||||
|
new Node\PropertyItem($this->name, $this->default)
|
||||||
|
],
|
||||||
|
$this->attributes,
|
||||||
|
$this->type,
|
||||||
|
$this->attributeGroups,
|
||||||
|
$this->hooks
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Builder;
|
||||||
|
|
||||||
|
use PhpParser\Builder;
|
||||||
|
use PhpParser\BuilderHelpers;
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Stmt;
|
||||||
|
|
||||||
|
class TraitUse implements Builder {
|
||||||
|
/** @var Node\Name[] */
|
||||||
|
protected array $traits = [];
|
||||||
|
/** @var Stmt\TraitUseAdaptation[] */
|
||||||
|
protected array $adaptations = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a trait use builder.
|
||||||
|
*
|
||||||
|
* @param Node\Name|string ...$traits Names of used traits
|
||||||
|
*/
|
||||||
|
public function __construct(...$traits) {
|
||||||
|
foreach ($traits as $trait) {
|
||||||
|
$this->and($trait);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds used trait.
|
||||||
|
*
|
||||||
|
* @param Node\Name|string $trait Trait name
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function and($trait) {
|
||||||
|
$this->traits[] = BuilderHelpers::normalizeName($trait);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds trait adaptation.
|
||||||
|
*
|
||||||
|
* @param Stmt\TraitUseAdaptation|Builder\TraitUseAdaptation $adaptation Trait adaptation
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function with($adaptation) {
|
||||||
|
$adaptation = BuilderHelpers::normalizeNode($adaptation);
|
||||||
|
|
||||||
|
if (!$adaptation instanceof Stmt\TraitUseAdaptation) {
|
||||||
|
throw new \LogicException('Adaptation must have type TraitUseAdaptation');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->adaptations[] = $adaptation;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the built node.
|
||||||
|
*
|
||||||
|
* @return Node The built node
|
||||||
|
*/
|
||||||
|
public function getNode(): Node {
|
||||||
|
return new Stmt\TraitUse($this->traits, $this->adaptations);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Builder;
|
||||||
|
|
||||||
|
use PhpParser\Builder;
|
||||||
|
use PhpParser\BuilderHelpers;
|
||||||
|
use PhpParser\Modifiers;
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Stmt;
|
||||||
|
|
||||||
|
class TraitUseAdaptation implements Builder {
|
||||||
|
private const TYPE_UNDEFINED = 0;
|
||||||
|
private const TYPE_ALIAS = 1;
|
||||||
|
private const TYPE_PRECEDENCE = 2;
|
||||||
|
|
||||||
|
protected int $type;
|
||||||
|
protected ?Node\Name $trait;
|
||||||
|
protected Node\Identifier $method;
|
||||||
|
protected ?int $modifier = null;
|
||||||
|
protected ?Node\Identifier $alias = null;
|
||||||
|
/** @var Node\Name[] */
|
||||||
|
protected array $insteadof = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a trait use adaptation builder.
|
||||||
|
*
|
||||||
|
* @param Node\Name|string|null $trait Name of adapted trait
|
||||||
|
* @param Node\Identifier|string $method Name of adapted method
|
||||||
|
*/
|
||||||
|
public function __construct($trait, $method) {
|
||||||
|
$this->type = self::TYPE_UNDEFINED;
|
||||||
|
|
||||||
|
$this->trait = is_null($trait) ? null : BuilderHelpers::normalizeName($trait);
|
||||||
|
$this->method = BuilderHelpers::normalizeIdentifier($method);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets alias of method.
|
||||||
|
*
|
||||||
|
* @param Node\Identifier|string $alias Alias for adapted method
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function as($alias) {
|
||||||
|
if ($this->type === self::TYPE_UNDEFINED) {
|
||||||
|
$this->type = self::TYPE_ALIAS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->type !== self::TYPE_ALIAS) {
|
||||||
|
throw new \LogicException('Cannot set alias for not alias adaptation buider');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->alias = BuilderHelpers::normalizeIdentifier($alias);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets adapted method public.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makePublic() {
|
||||||
|
$this->setModifier(Modifiers::PUBLIC);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets adapted method protected.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makeProtected() {
|
||||||
|
$this->setModifier(Modifiers::PROTECTED);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets adapted method private.
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function makePrivate() {
|
||||||
|
$this->setModifier(Modifiers::PRIVATE);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds overwritten traits.
|
||||||
|
*
|
||||||
|
* @param Node\Name|string ...$traits Traits for overwrite
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function insteadof(...$traits) {
|
||||||
|
if ($this->type === self::TYPE_UNDEFINED) {
|
||||||
|
if (is_null($this->trait)) {
|
||||||
|
throw new \LogicException('Precedence adaptation must have trait');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->type = self::TYPE_PRECEDENCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->type !== self::TYPE_PRECEDENCE) {
|
||||||
|
throw new \LogicException('Cannot add overwritten traits for not precedence adaptation buider');
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($traits as $trait) {
|
||||||
|
$this->insteadof[] = BuilderHelpers::normalizeName($trait);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setModifier(int $modifier): void {
|
||||||
|
if ($this->type === self::TYPE_UNDEFINED) {
|
||||||
|
$this->type = self::TYPE_ALIAS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->type !== self::TYPE_ALIAS) {
|
||||||
|
throw new \LogicException('Cannot set access modifier for not alias adaptation buider');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_null($this->modifier)) {
|
||||||
|
$this->modifier = $modifier;
|
||||||
|
} else {
|
||||||
|
throw new \LogicException('Multiple access type modifiers are not allowed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the built node.
|
||||||
|
*
|
||||||
|
* @return Node The built node
|
||||||
|
*/
|
||||||
|
public function getNode(): Node {
|
||||||
|
switch ($this->type) {
|
||||||
|
case self::TYPE_ALIAS:
|
||||||
|
return new Stmt\TraitUseAdaptation\Alias($this->trait, $this->method, $this->modifier, $this->alias);
|
||||||
|
case self::TYPE_PRECEDENCE:
|
||||||
|
return new Stmt\TraitUseAdaptation\Precedence($this->trait, $this->method, $this->insteadof);
|
||||||
|
default:
|
||||||
|
throw new \LogicException('Type of adaptation is not defined');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Builder;
|
||||||
|
|
||||||
|
use PhpParser;
|
||||||
|
use PhpParser\BuilderHelpers;
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Stmt;
|
||||||
|
|
||||||
|
class Trait_ extends Declaration {
|
||||||
|
protected string $name;
|
||||||
|
/** @var list<Stmt\TraitUse> */
|
||||||
|
protected array $uses = [];
|
||||||
|
/** @var list<Stmt\ClassConst> */
|
||||||
|
protected array $constants = [];
|
||||||
|
/** @var list<Stmt\Property> */
|
||||||
|
protected array $properties = [];
|
||||||
|
/** @var list<Stmt\ClassMethod> */
|
||||||
|
protected array $methods = [];
|
||||||
|
/** @var list<Node\AttributeGroup> */
|
||||||
|
protected array $attributeGroups = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an interface builder.
|
||||||
|
*
|
||||||
|
* @param string $name Name of the interface
|
||||||
|
*/
|
||||||
|
public function __construct(string $name) {
|
||||||
|
$this->name = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a statement.
|
||||||
|
*
|
||||||
|
* @param Stmt|PhpParser\Builder $stmt The statement to add
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addStmt($stmt) {
|
||||||
|
$stmt = BuilderHelpers::normalizeNode($stmt);
|
||||||
|
|
||||||
|
if ($stmt instanceof Stmt\Property) {
|
||||||
|
$this->properties[] = $stmt;
|
||||||
|
} elseif ($stmt instanceof Stmt\ClassMethod) {
|
||||||
|
$this->methods[] = $stmt;
|
||||||
|
} elseif ($stmt instanceof Stmt\TraitUse) {
|
||||||
|
$this->uses[] = $stmt;
|
||||||
|
} elseif ($stmt instanceof Stmt\ClassConst) {
|
||||||
|
$this->constants[] = $stmt;
|
||||||
|
} else {
|
||||||
|
throw new \LogicException(sprintf('Unexpected node of type "%s"', $stmt->getType()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an attribute group.
|
||||||
|
*
|
||||||
|
* @param Node\Attribute|Node\AttributeGroup $attribute
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function addAttribute($attribute) {
|
||||||
|
$this->attributeGroups[] = BuilderHelpers::normalizeAttribute($attribute);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the built trait node.
|
||||||
|
*
|
||||||
|
* @return Stmt\Trait_ The built interface node
|
||||||
|
*/
|
||||||
|
public function getNode(): PhpParser\Node {
|
||||||
|
return new Stmt\Trait_(
|
||||||
|
$this->name, [
|
||||||
|
'stmts' => array_merge($this->uses, $this->constants, $this->properties, $this->methods),
|
||||||
|
'attrGroups' => $this->attributeGroups,
|
||||||
|
], $this->attributes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Builder;
|
||||||
|
|
||||||
|
use PhpParser\Builder;
|
||||||
|
use PhpParser\BuilderHelpers;
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Stmt;
|
||||||
|
|
||||||
|
class Use_ implements Builder {
|
||||||
|
protected Node\Name $name;
|
||||||
|
/** @var Stmt\Use_::TYPE_* */
|
||||||
|
protected int $type;
|
||||||
|
protected ?string $alias = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a name use (alias) builder.
|
||||||
|
*
|
||||||
|
* @param Node\Name|string $name Name of the entity (namespace, class, function, constant) to alias
|
||||||
|
* @param Stmt\Use_::TYPE_* $type One of the Stmt\Use_::TYPE_* constants
|
||||||
|
*/
|
||||||
|
public function __construct($name, int $type) {
|
||||||
|
$this->name = BuilderHelpers::normalizeName($name);
|
||||||
|
$this->type = $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets alias for used name.
|
||||||
|
*
|
||||||
|
* @param string $alias Alias to use (last component of full name by default)
|
||||||
|
*
|
||||||
|
* @return $this The builder instance (for fluid interface)
|
||||||
|
*/
|
||||||
|
public function as(string $alias) {
|
||||||
|
$this->alias = $alias;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the built node.
|
||||||
|
*
|
||||||
|
* @return Stmt\Use_ The built node
|
||||||
|
*/
|
||||||
|
public function getNode(): Node {
|
||||||
|
return new Stmt\Use_([
|
||||||
|
new Node\UseItem($this->name, $this->alias)
|
||||||
|
], $this->type);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser;
|
||||||
|
|
||||||
|
use PhpParser\Node\Arg;
|
||||||
|
use PhpParser\Node\Expr;
|
||||||
|
use PhpParser\Node\Expr\BinaryOp\Concat;
|
||||||
|
use PhpParser\Node\Identifier;
|
||||||
|
use PhpParser\Node\Name;
|
||||||
|
use PhpParser\Node\Scalar\String_;
|
||||||
|
use PhpParser\Node\Stmt\Use_;
|
||||||
|
|
||||||
|
class BuilderFactory {
|
||||||
|
/**
|
||||||
|
* Creates an attribute node.
|
||||||
|
*
|
||||||
|
* @param string|Name $name Name of the attribute
|
||||||
|
* @param array $args Attribute named arguments
|
||||||
|
*/
|
||||||
|
public function attribute($name, array $args = []): Node\Attribute {
|
||||||
|
return new Node\Attribute(
|
||||||
|
BuilderHelpers::normalizeName($name),
|
||||||
|
$this->args($args)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a namespace builder.
|
||||||
|
*
|
||||||
|
* @param null|string|Node\Name $name Name of the namespace
|
||||||
|
*
|
||||||
|
* @return Builder\Namespace_ The created namespace builder
|
||||||
|
*/
|
||||||
|
public function namespace($name): Builder\Namespace_ {
|
||||||
|
return new Builder\Namespace_($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a class builder.
|
||||||
|
*
|
||||||
|
* @param string $name Name of the class
|
||||||
|
*
|
||||||
|
* @return Builder\Class_ The created class builder
|
||||||
|
*/
|
||||||
|
public function class(string $name): Builder\Class_ {
|
||||||
|
return new Builder\Class_($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an interface builder.
|
||||||
|
*
|
||||||
|
* @param string $name Name of the interface
|
||||||
|
*
|
||||||
|
* @return Builder\Interface_ The created interface builder
|
||||||
|
*/
|
||||||
|
public function interface(string $name): Builder\Interface_ {
|
||||||
|
return new Builder\Interface_($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a trait builder.
|
||||||
|
*
|
||||||
|
* @param string $name Name of the trait
|
||||||
|
*
|
||||||
|
* @return Builder\Trait_ The created trait builder
|
||||||
|
*/
|
||||||
|
public function trait(string $name): Builder\Trait_ {
|
||||||
|
return new Builder\Trait_($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an enum builder.
|
||||||
|
*
|
||||||
|
* @param string $name Name of the enum
|
||||||
|
*
|
||||||
|
* @return Builder\Enum_ The created enum builder
|
||||||
|
*/
|
||||||
|
public function enum(string $name): Builder\Enum_ {
|
||||||
|
return new Builder\Enum_($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a trait use builder.
|
||||||
|
*
|
||||||
|
* @param Node\Name|string ...$traits Trait names
|
||||||
|
*
|
||||||
|
* @return Builder\TraitUse The created trait use builder
|
||||||
|
*/
|
||||||
|
public function useTrait(...$traits): Builder\TraitUse {
|
||||||
|
return new Builder\TraitUse(...$traits);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a trait use adaptation builder.
|
||||||
|
*
|
||||||
|
* @param Node\Name|string|null $trait Trait name
|
||||||
|
* @param Node\Identifier|string $method Method name
|
||||||
|
*
|
||||||
|
* @return Builder\TraitUseAdaptation The created trait use adaptation builder
|
||||||
|
*/
|
||||||
|
public function traitUseAdaptation($trait, $method = null): Builder\TraitUseAdaptation {
|
||||||
|
if ($method === null) {
|
||||||
|
$method = $trait;
|
||||||
|
$trait = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Builder\TraitUseAdaptation($trait, $method);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a method builder.
|
||||||
|
*
|
||||||
|
* @param string $name Name of the method
|
||||||
|
*
|
||||||
|
* @return Builder\Method The created method builder
|
||||||
|
*/
|
||||||
|
public function method(string $name): Builder\Method {
|
||||||
|
return new Builder\Method($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a parameter builder.
|
||||||
|
*
|
||||||
|
* @param string $name Name of the parameter
|
||||||
|
*
|
||||||
|
* @return Builder\Param The created parameter builder
|
||||||
|
*/
|
||||||
|
public function param(string $name): Builder\Param {
|
||||||
|
return new Builder\Param($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a property builder.
|
||||||
|
*
|
||||||
|
* @param string $name Name of the property
|
||||||
|
*
|
||||||
|
* @return Builder\Property The created property builder
|
||||||
|
*/
|
||||||
|
public function property(string $name): Builder\Property {
|
||||||
|
return new Builder\Property($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a function builder.
|
||||||
|
*
|
||||||
|
* @param string $name Name of the function
|
||||||
|
*
|
||||||
|
* @return Builder\Function_ The created function builder
|
||||||
|
*/
|
||||||
|
public function function(string $name): Builder\Function_ {
|
||||||
|
return new Builder\Function_($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a namespace/class use builder.
|
||||||
|
*
|
||||||
|
* @param Node\Name|string $name Name of the entity (namespace or class) to alias
|
||||||
|
*
|
||||||
|
* @return Builder\Use_ The created use builder
|
||||||
|
*/
|
||||||
|
public function use($name): Builder\Use_ {
|
||||||
|
return new Builder\Use_($name, Use_::TYPE_NORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a function use builder.
|
||||||
|
*
|
||||||
|
* @param Node\Name|string $name Name of the function to alias
|
||||||
|
*
|
||||||
|
* @return Builder\Use_ The created use function builder
|
||||||
|
*/
|
||||||
|
public function useFunction($name): Builder\Use_ {
|
||||||
|
return new Builder\Use_($name, Use_::TYPE_FUNCTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a constant use builder.
|
||||||
|
*
|
||||||
|
* @param Node\Name|string $name Name of the const to alias
|
||||||
|
*
|
||||||
|
* @return Builder\Use_ The created use const builder
|
||||||
|
*/
|
||||||
|
public function useConst($name): Builder\Use_ {
|
||||||
|
return new Builder\Use_($name, Use_::TYPE_CONSTANT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a class constant builder.
|
||||||
|
*
|
||||||
|
* @param string|Identifier $name Name
|
||||||
|
* @param Node\Expr|bool|null|int|float|string|array $value Value
|
||||||
|
*
|
||||||
|
* @return Builder\ClassConst The created use const builder
|
||||||
|
*/
|
||||||
|
public function classConst($name, $value): Builder\ClassConst {
|
||||||
|
return new Builder\ClassConst($name, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an enum case builder.
|
||||||
|
*
|
||||||
|
* @param string|Identifier $name Name
|
||||||
|
*
|
||||||
|
* @return Builder\EnumCase The created use const builder
|
||||||
|
*/
|
||||||
|
public function enumCase($name): Builder\EnumCase {
|
||||||
|
return new Builder\EnumCase($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates node a for a literal value.
|
||||||
|
*
|
||||||
|
* @param Expr|bool|null|int|float|string|array|\UnitEnum $value $value
|
||||||
|
*/
|
||||||
|
public function val($value): Expr {
|
||||||
|
return BuilderHelpers::normalizeValue($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates variable node.
|
||||||
|
*
|
||||||
|
* @param string|Expr $name Name
|
||||||
|
*/
|
||||||
|
public function var($name): Expr\Variable {
|
||||||
|
if (!\is_string($name) && !$name instanceof Expr) {
|
||||||
|
throw new \LogicException('Variable name must be string or Expr');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Expr\Variable($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes an argument list.
|
||||||
|
*
|
||||||
|
* Creates Arg nodes for all arguments and converts literal values to expressions.
|
||||||
|
*
|
||||||
|
* @param array $args List of arguments to normalize
|
||||||
|
*
|
||||||
|
* @return list<Arg>
|
||||||
|
*/
|
||||||
|
public function args(array $args): array {
|
||||||
|
$normalizedArgs = [];
|
||||||
|
foreach ($args as $key => $arg) {
|
||||||
|
if (!($arg instanceof Arg)) {
|
||||||
|
$arg = new Arg(BuilderHelpers::normalizeValue($arg));
|
||||||
|
}
|
||||||
|
if (\is_string($key)) {
|
||||||
|
$arg->name = BuilderHelpers::normalizeIdentifier($key);
|
||||||
|
}
|
||||||
|
$normalizedArgs[] = $arg;
|
||||||
|
}
|
||||||
|
return $normalizedArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a function call node.
|
||||||
|
*
|
||||||
|
* @param string|Name|Expr $name Function name
|
||||||
|
* @param array $args Function arguments
|
||||||
|
*/
|
||||||
|
public function funcCall($name, array $args = []): Expr\FuncCall {
|
||||||
|
return new Expr\FuncCall(
|
||||||
|
BuilderHelpers::normalizeNameOrExpr($name),
|
||||||
|
$this->args($args)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a method call node.
|
||||||
|
*
|
||||||
|
* @param Expr $var Variable the method is called on
|
||||||
|
* @param string|Identifier|Expr $name Method name
|
||||||
|
* @param array $args Method arguments
|
||||||
|
*/
|
||||||
|
public function methodCall(Expr $var, $name, array $args = []): Expr\MethodCall {
|
||||||
|
return new Expr\MethodCall(
|
||||||
|
$var,
|
||||||
|
BuilderHelpers::normalizeIdentifierOrExpr($name),
|
||||||
|
$this->args($args)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a static method call node.
|
||||||
|
*
|
||||||
|
* @param string|Name|Expr $class Class name
|
||||||
|
* @param string|Identifier|Expr $name Method name
|
||||||
|
* @param array $args Method arguments
|
||||||
|
*/
|
||||||
|
public function staticCall($class, $name, array $args = []): Expr\StaticCall {
|
||||||
|
return new Expr\StaticCall(
|
||||||
|
BuilderHelpers::normalizeNameOrExpr($class),
|
||||||
|
BuilderHelpers::normalizeIdentifierOrExpr($name),
|
||||||
|
$this->args($args)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an object creation node.
|
||||||
|
*
|
||||||
|
* @param string|Name|Expr $class Class name
|
||||||
|
* @param array $args Constructor arguments
|
||||||
|
*/
|
||||||
|
public function new($class, array $args = []): Expr\New_ {
|
||||||
|
return new Expr\New_(
|
||||||
|
BuilderHelpers::normalizeNameOrExpr($class),
|
||||||
|
$this->args($args)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a constant fetch node.
|
||||||
|
*
|
||||||
|
* @param string|Name $name Constant name
|
||||||
|
*/
|
||||||
|
public function constFetch($name): Expr\ConstFetch {
|
||||||
|
return new Expr\ConstFetch(BuilderHelpers::normalizeName($name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a property fetch node.
|
||||||
|
*
|
||||||
|
* @param Expr $var Variable holding object
|
||||||
|
* @param string|Identifier|Expr $name Property name
|
||||||
|
*/
|
||||||
|
public function propertyFetch(Expr $var, $name): Expr\PropertyFetch {
|
||||||
|
return new Expr\PropertyFetch($var, BuilderHelpers::normalizeIdentifierOrExpr($name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a class constant fetch node.
|
||||||
|
*
|
||||||
|
* @param string|Name|Expr $class Class name
|
||||||
|
* @param string|Identifier|Expr $name Constant name
|
||||||
|
*/
|
||||||
|
public function classConstFetch($class, $name): Expr\ClassConstFetch {
|
||||||
|
return new Expr\ClassConstFetch(
|
||||||
|
BuilderHelpers::normalizeNameOrExpr($class),
|
||||||
|
BuilderHelpers::normalizeIdentifierOrExpr($name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates nested Concat nodes from a list of expressions.
|
||||||
|
*
|
||||||
|
* @param Expr|string ...$exprs Expressions or literal strings
|
||||||
|
*/
|
||||||
|
public function concat(...$exprs): Concat {
|
||||||
|
$numExprs = count($exprs);
|
||||||
|
if ($numExprs < 2) {
|
||||||
|
throw new \LogicException('Expected at least two expressions');
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastConcat = $this->normalizeStringExpr($exprs[0]);
|
||||||
|
for ($i = 1; $i < $numExprs; $i++) {
|
||||||
|
$lastConcat = new Concat($lastConcat, $this->normalizeStringExpr($exprs[$i]));
|
||||||
|
}
|
||||||
|
return $lastConcat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string|Expr $expr
|
||||||
|
*/
|
||||||
|
private function normalizeStringExpr($expr): Expr {
|
||||||
|
if ($expr instanceof Expr) {
|
||||||
|
return $expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\is_string($expr)) {
|
||||||
|
return new String_($expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \LogicException('Expected string or Expr');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser;
|
||||||
|
|
||||||
|
use PhpParser\Node\ComplexType;
|
||||||
|
use PhpParser\Node\Expr;
|
||||||
|
use PhpParser\Node\Identifier;
|
||||||
|
use PhpParser\Node\Name;
|
||||||
|
use PhpParser\Node\Name\FullyQualified;
|
||||||
|
use PhpParser\Node\NullableType;
|
||||||
|
use PhpParser\Node\Scalar;
|
||||||
|
use PhpParser\Node\Stmt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class defines helpers used in the implementation of builders. Don't use it directly.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class BuilderHelpers {
|
||||||
|
/**
|
||||||
|
* Normalizes a node: Converts builder objects to nodes.
|
||||||
|
*
|
||||||
|
* @param Node|Builder $node The node to normalize
|
||||||
|
*
|
||||||
|
* @return Node The normalized node
|
||||||
|
*/
|
||||||
|
public static function normalizeNode($node): Node {
|
||||||
|
if ($node instanceof Builder) {
|
||||||
|
return $node->getNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($node instanceof Node) {
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \LogicException('Expected node or builder object');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a node to a statement.
|
||||||
|
*
|
||||||
|
* Expressions are wrapped in a Stmt\Expression node.
|
||||||
|
*
|
||||||
|
* @param Node|Builder $node The node to normalize
|
||||||
|
*
|
||||||
|
* @return Stmt The normalized statement node
|
||||||
|
*/
|
||||||
|
public static function normalizeStmt($node): Stmt {
|
||||||
|
$node = self::normalizeNode($node);
|
||||||
|
if ($node instanceof Stmt) {
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($node instanceof Expr) {
|
||||||
|
return new Stmt\Expression($node);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \LogicException('Expected statement or expression node');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes strings to Identifier.
|
||||||
|
*
|
||||||
|
* @param string|Identifier $name The identifier to normalize
|
||||||
|
*
|
||||||
|
* @return Identifier The normalized identifier
|
||||||
|
*/
|
||||||
|
public static function normalizeIdentifier($name): Identifier {
|
||||||
|
if ($name instanceof Identifier) {
|
||||||
|
return $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\is_string($name)) {
|
||||||
|
return new Identifier($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \LogicException('Expected string or instance of Node\Identifier');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes strings to Identifier, also allowing expressions.
|
||||||
|
*
|
||||||
|
* @param string|Identifier|Expr $name The identifier to normalize
|
||||||
|
*
|
||||||
|
* @return Identifier|Expr The normalized identifier or expression
|
||||||
|
*/
|
||||||
|
public static function normalizeIdentifierOrExpr($name) {
|
||||||
|
if ($name instanceof Identifier || $name instanceof Expr) {
|
||||||
|
return $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\is_string($name)) {
|
||||||
|
return new Identifier($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \LogicException('Expected string or instance of Node\Identifier or Node\Expr');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a name: Converts string names to Name nodes.
|
||||||
|
*
|
||||||
|
* @param Name|string $name The name to normalize
|
||||||
|
*
|
||||||
|
* @return Name The normalized name
|
||||||
|
*/
|
||||||
|
public static function normalizeName($name): Name {
|
||||||
|
if ($name instanceof Name) {
|
||||||
|
return $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($name)) {
|
||||||
|
if (!$name) {
|
||||||
|
throw new \LogicException('Name cannot be empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($name[0] === '\\') {
|
||||||
|
return new Name\FullyQualified(substr($name, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 === strpos($name, 'namespace\\')) {
|
||||||
|
return new Name\Relative(substr($name, strlen('namespace\\')));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Name($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \LogicException('Name must be a string or an instance of Node\Name');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a name: Converts string names to Name nodes, while also allowing expressions.
|
||||||
|
*
|
||||||
|
* @param Expr|Name|string $name The name to normalize
|
||||||
|
*
|
||||||
|
* @return Name|Expr The normalized name or expression
|
||||||
|
*/
|
||||||
|
public static function normalizeNameOrExpr($name) {
|
||||||
|
if ($name instanceof Expr) {
|
||||||
|
return $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_string($name) && !($name instanceof Name)) {
|
||||||
|
throw new \LogicException(
|
||||||
|
'Name must be a string or an instance of Node\Name or Node\Expr'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::normalizeName($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a type: Converts plain-text type names into proper AST representation.
|
||||||
|
*
|
||||||
|
* In particular, builtin types become Identifiers, custom types become Names and nullables
|
||||||
|
* are wrapped in NullableType nodes.
|
||||||
|
*
|
||||||
|
* @param string|Name|Identifier|ComplexType $type The type to normalize
|
||||||
|
*
|
||||||
|
* @return Name|Identifier|ComplexType The normalized type
|
||||||
|
*/
|
||||||
|
public static function normalizeType($type) {
|
||||||
|
if (!is_string($type)) {
|
||||||
|
if (
|
||||||
|
!$type instanceof Name && !$type instanceof Identifier &&
|
||||||
|
!$type instanceof ComplexType
|
||||||
|
) {
|
||||||
|
throw new \LogicException(
|
||||||
|
'Type must be a string, or an instance of Name, Identifier or ComplexType'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nullable = false;
|
||||||
|
if (strlen($type) > 0 && $type[0] === '?') {
|
||||||
|
$nullable = true;
|
||||||
|
$type = substr($type, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$builtinTypes = [
|
||||||
|
'array',
|
||||||
|
'callable',
|
||||||
|
'bool',
|
||||||
|
'int',
|
||||||
|
'float',
|
||||||
|
'string',
|
||||||
|
'iterable',
|
||||||
|
'void',
|
||||||
|
'object',
|
||||||
|
'null',
|
||||||
|
'false',
|
||||||
|
'mixed',
|
||||||
|
'never',
|
||||||
|
'true',
|
||||||
|
];
|
||||||
|
|
||||||
|
$lowerType = strtolower($type);
|
||||||
|
if (in_array($lowerType, $builtinTypes)) {
|
||||||
|
$type = new Identifier($lowerType);
|
||||||
|
} else {
|
||||||
|
$type = self::normalizeName($type);
|
||||||
|
}
|
||||||
|
|
||||||
|
$notNullableTypes = [
|
||||||
|
'void', 'mixed', 'never',
|
||||||
|
];
|
||||||
|
if ($nullable && in_array((string) $type, $notNullableTypes)) {
|
||||||
|
throw new \LogicException(sprintf('%s type cannot be nullable', $type));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $nullable ? new NullableType($type) : $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a value: Converts nulls, booleans, integers,
|
||||||
|
* floats, strings and arrays into their respective nodes
|
||||||
|
*
|
||||||
|
* @param Node\Expr|bool|null|int|float|string|array|\UnitEnum $value The value to normalize
|
||||||
|
*
|
||||||
|
* @return Expr The normalized value
|
||||||
|
*/
|
||||||
|
public static function normalizeValue($value): Expr {
|
||||||
|
if ($value instanceof Node\Expr) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_null($value)) {
|
||||||
|
return new Expr\ConstFetch(
|
||||||
|
new Name('null')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_bool($value)) {
|
||||||
|
return new Expr\ConstFetch(
|
||||||
|
new Name($value ? 'true' : 'false')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_int($value)) {
|
||||||
|
return new Scalar\Int_($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_float($value)) {
|
||||||
|
return new Scalar\Float_($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($value)) {
|
||||||
|
return new Scalar\String_($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($value)) {
|
||||||
|
$items = [];
|
||||||
|
$lastKey = -1;
|
||||||
|
foreach ($value as $itemKey => $itemValue) {
|
||||||
|
// for consecutive, numeric keys don't generate keys
|
||||||
|
if (null !== $lastKey && ++$lastKey === $itemKey) {
|
||||||
|
$items[] = new Node\ArrayItem(
|
||||||
|
self::normalizeValue($itemValue)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$lastKey = null;
|
||||||
|
$items[] = new Node\ArrayItem(
|
||||||
|
self::normalizeValue($itemValue),
|
||||||
|
self::normalizeValue($itemKey)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Expr\Array_($items);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof \UnitEnum) {
|
||||||
|
return new Expr\ClassConstFetch(new FullyQualified(\get_class($value)), new Identifier($value->name));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \LogicException('Invalid value');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a doc comment: Converts plain strings to PhpParser\Comment\Doc.
|
||||||
|
*
|
||||||
|
* @param Comment\Doc|string $docComment The doc comment to normalize
|
||||||
|
*
|
||||||
|
* @return Comment\Doc The normalized doc comment
|
||||||
|
*/
|
||||||
|
public static function normalizeDocComment($docComment): Comment\Doc {
|
||||||
|
if ($docComment instanceof Comment\Doc) {
|
||||||
|
return $docComment;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($docComment)) {
|
||||||
|
return new Comment\Doc($docComment);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \LogicException('Doc comment must be a string or an instance of PhpParser\Comment\Doc');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a attribute: Converts attribute to the Attribute Group if needed.
|
||||||
|
*
|
||||||
|
* @param Node\Attribute|Node\AttributeGroup $attribute
|
||||||
|
*
|
||||||
|
* @return Node\AttributeGroup The Attribute Group
|
||||||
|
*/
|
||||||
|
public static function normalizeAttribute($attribute): Node\AttributeGroup {
|
||||||
|
if ($attribute instanceof Node\AttributeGroup) {
|
||||||
|
return $attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!($attribute instanceof Node\Attribute)) {
|
||||||
|
throw new \LogicException('Attribute must be an instance of PhpParser\Node\Attribute or PhpParser\Node\AttributeGroup');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Node\AttributeGroup([$attribute]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a modifier and returns new modifier bitmask.
|
||||||
|
*
|
||||||
|
* @param int $modifiers Existing modifiers
|
||||||
|
* @param int $modifier Modifier to set
|
||||||
|
*
|
||||||
|
* @return int New modifiers
|
||||||
|
*/
|
||||||
|
public static function addModifier(int $modifiers, int $modifier): int {
|
||||||
|
Modifiers::verifyModifier($modifiers, $modifier);
|
||||||
|
return $modifiers | $modifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a modifier and returns new modifier bitmask.
|
||||||
|
* @return int New modifiers
|
||||||
|
*/
|
||||||
|
public static function addClassModifier(int $existingModifiers, int $modifierToSet): int {
|
||||||
|
Modifiers::verifyClassModifier($existingModifiers, $modifierToSet);
|
||||||
|
return $existingModifiers | $modifierToSet;
|
||||||
|
}
|
||||||
|
}
|
||||||
+209
@@ -0,0 +1,209 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser;
|
||||||
|
|
||||||
|
class Comment implements \JsonSerializable {
|
||||||
|
protected string $text;
|
||||||
|
protected int $startLine;
|
||||||
|
protected int $startFilePos;
|
||||||
|
protected int $startTokenPos;
|
||||||
|
protected int $endLine;
|
||||||
|
protected int $endFilePos;
|
||||||
|
protected int $endTokenPos;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a comment node.
|
||||||
|
*
|
||||||
|
* @param string $text Comment text (including comment delimiters like /*)
|
||||||
|
* @param int $startLine Line number the comment started on
|
||||||
|
* @param int $startFilePos File offset the comment started on
|
||||||
|
* @param int $startTokenPos Token offset the comment started on
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
string $text,
|
||||||
|
int $startLine = -1, int $startFilePos = -1, int $startTokenPos = -1,
|
||||||
|
int $endLine = -1, int $endFilePos = -1, int $endTokenPos = -1
|
||||||
|
) {
|
||||||
|
$this->text = $text;
|
||||||
|
$this->startLine = $startLine;
|
||||||
|
$this->startFilePos = $startFilePos;
|
||||||
|
$this->startTokenPos = $startTokenPos;
|
||||||
|
$this->endLine = $endLine;
|
||||||
|
$this->endFilePos = $endFilePos;
|
||||||
|
$this->endTokenPos = $endTokenPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the comment text.
|
||||||
|
*
|
||||||
|
* @return string The comment text (including comment delimiters like /*)
|
||||||
|
*/
|
||||||
|
public function getText(): string {
|
||||||
|
return $this->text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the line number the comment started on.
|
||||||
|
*
|
||||||
|
* @return int Line number (or -1 if not available)
|
||||||
|
* @phpstan-return -1|positive-int
|
||||||
|
*/
|
||||||
|
public function getStartLine(): int {
|
||||||
|
return $this->startLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the file offset the comment started on.
|
||||||
|
*
|
||||||
|
* @return int File offset (or -1 if not available)
|
||||||
|
*/
|
||||||
|
public function getStartFilePos(): int {
|
||||||
|
return $this->startFilePos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the token offset the comment started on.
|
||||||
|
*
|
||||||
|
* @return int Token offset (or -1 if not available)
|
||||||
|
*/
|
||||||
|
public function getStartTokenPos(): int {
|
||||||
|
return $this->startTokenPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the line number the comment ends on.
|
||||||
|
*
|
||||||
|
* @return int Line number (or -1 if not available)
|
||||||
|
* @phpstan-return -1|positive-int
|
||||||
|
*/
|
||||||
|
public function getEndLine(): int {
|
||||||
|
return $this->endLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the file offset the comment ends on.
|
||||||
|
*
|
||||||
|
* @return int File offset (or -1 if not available)
|
||||||
|
*/
|
||||||
|
public function getEndFilePos(): int {
|
||||||
|
return $this->endFilePos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the token offset the comment ends on.
|
||||||
|
*
|
||||||
|
* @return int Token offset (or -1 if not available)
|
||||||
|
*/
|
||||||
|
public function getEndTokenPos(): int {
|
||||||
|
return $this->endTokenPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the comment text.
|
||||||
|
*
|
||||||
|
* @return string The comment text (including comment delimiters like /*)
|
||||||
|
*/
|
||||||
|
public function __toString(): string {
|
||||||
|
return $this->text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the reformatted comment text.
|
||||||
|
*
|
||||||
|
* "Reformatted" here means that we try to clean up the whitespace at the
|
||||||
|
* starts of the lines. This is necessary because we receive the comments
|
||||||
|
* without leading whitespace on the first line, but with leading whitespace
|
||||||
|
* on all subsequent lines.
|
||||||
|
*
|
||||||
|
* Additionally, this normalizes CRLF newlines to LF newlines.
|
||||||
|
*/
|
||||||
|
public function getReformattedText(): string {
|
||||||
|
$text = str_replace("\r\n", "\n", $this->text);
|
||||||
|
$newlinePos = strpos($text, "\n");
|
||||||
|
if (false === $newlinePos) {
|
||||||
|
// Single line comments don't need further processing
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
if (preg_match('(^.*(?:\n\s+\*.*)+$)', $text)) {
|
||||||
|
// Multi line comment of the type
|
||||||
|
//
|
||||||
|
// /*
|
||||||
|
// * Some text.
|
||||||
|
// * Some more text.
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
// is handled by replacing the whitespace sequences before the * by a single space
|
||||||
|
return preg_replace('(^\s+\*)m', ' *', $text);
|
||||||
|
}
|
||||||
|
if (preg_match('(^/\*\*?\s*\n)', $text) && preg_match('(\n(\s*)\*/$)', $text, $matches)) {
|
||||||
|
// Multi line comment of the type
|
||||||
|
//
|
||||||
|
// /*
|
||||||
|
// Some text.
|
||||||
|
// Some more text.
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
// is handled by removing the whitespace sequence on the line before the closing
|
||||||
|
// */ on all lines. So if the last line is " */", then " " is removed at the
|
||||||
|
// start of all lines.
|
||||||
|
return preg_replace('(^' . preg_quote($matches[1]) . ')m', '', $text);
|
||||||
|
}
|
||||||
|
if (preg_match('(^/\*\*?\s*(?!\s))', $text, $matches)) {
|
||||||
|
// Multi line comment of the type
|
||||||
|
//
|
||||||
|
// /* Some text.
|
||||||
|
// Some more text.
|
||||||
|
// Indented text.
|
||||||
|
// Even more text. */
|
||||||
|
//
|
||||||
|
// is handled by removing the difference between the shortest whitespace prefix on all
|
||||||
|
// lines and the length of the "/* " opening sequence.
|
||||||
|
$prefixLen = $this->getShortestWhitespacePrefixLen(substr($text, $newlinePos + 1));
|
||||||
|
$removeLen = $prefixLen - strlen($matches[0]);
|
||||||
|
return preg_replace('(^\s{' . $removeLen . '})m', '', $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No idea how to format this comment, so simply return as is
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get length of shortest whitespace prefix (at the start of a line).
|
||||||
|
*
|
||||||
|
* If there is a line with no prefix whitespace, 0 is a valid return value.
|
||||||
|
*
|
||||||
|
* @param string $str String to check
|
||||||
|
* @return int Length in characters. Tabs count as single characters.
|
||||||
|
*/
|
||||||
|
private function getShortestWhitespacePrefixLen(string $str): int {
|
||||||
|
$lines = explode("\n", $str);
|
||||||
|
$shortestPrefixLen = \PHP_INT_MAX;
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
preg_match('(^\s*)', $line, $matches);
|
||||||
|
$prefixLen = strlen($matches[0]);
|
||||||
|
if ($prefixLen < $shortestPrefixLen) {
|
||||||
|
$shortestPrefixLen = $prefixLen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $shortestPrefixLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{nodeType:string, text:mixed, line:mixed, filePos:mixed}
|
||||||
|
*/
|
||||||
|
public function jsonSerialize(): array {
|
||||||
|
// Technically not a node, but we make it look like one anyway
|
||||||
|
$type = $this instanceof Comment\Doc ? 'Comment_Doc' : 'Comment';
|
||||||
|
return [
|
||||||
|
'nodeType' => $type,
|
||||||
|
'text' => $this->text,
|
||||||
|
// TODO: Rename these to include "start".
|
||||||
|
'line' => $this->startLine,
|
||||||
|
'filePos' => $this->startFilePos,
|
||||||
|
'tokenPos' => $this->startTokenPos,
|
||||||
|
'endLine' => $this->endLine,
|
||||||
|
'endFilePos' => $this->endFilePos,
|
||||||
|
'endTokenPos' => $this->endTokenPos,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Comment;
|
||||||
|
|
||||||
|
class Doc extends \PhpParser\Comment {
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser;
|
||||||
|
|
||||||
|
class ConstExprEvaluationException extends \Exception {
|
||||||
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser;
|
||||||
|
|
||||||
|
use PhpParser\Node\Expr;
|
||||||
|
use PhpParser\Node\Scalar;
|
||||||
|
|
||||||
|
use function array_merge;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates constant expressions.
|
||||||
|
*
|
||||||
|
* This evaluator is able to evaluate all constant expressions (as defined by PHP), which can be
|
||||||
|
* evaluated without further context. If a subexpression is not of this type, a user-provided
|
||||||
|
* fallback evaluator is invoked. To support all constant expressions that are also supported by
|
||||||
|
* PHP (and not already handled by this class), the fallback evaluator must be able to handle the
|
||||||
|
* following node types:
|
||||||
|
*
|
||||||
|
* * All Scalar\MagicConst\* nodes.
|
||||||
|
* * Expr\ConstFetch nodes. Only null/false/true are already handled by this class.
|
||||||
|
* * Expr\ClassConstFetch nodes.
|
||||||
|
*
|
||||||
|
* The fallback evaluator should throw ConstExprEvaluationException for nodes it cannot evaluate.
|
||||||
|
*
|
||||||
|
* The evaluation is dependent on runtime configuration in two respects: Firstly, floating
|
||||||
|
* point to string conversions are affected by the precision ini setting. Secondly, they are also
|
||||||
|
* affected by the LC_NUMERIC locale.
|
||||||
|
*/
|
||||||
|
class ConstExprEvaluator {
|
||||||
|
/** @var callable|null */
|
||||||
|
private $fallbackEvaluator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a constant expression evaluator.
|
||||||
|
*
|
||||||
|
* The provided fallback evaluator is invoked whenever a subexpression cannot be evaluated. See
|
||||||
|
* class doc comment for more information.
|
||||||
|
*
|
||||||
|
* @param callable|null $fallbackEvaluator To call if subexpression cannot be evaluated
|
||||||
|
*/
|
||||||
|
public function __construct(?callable $fallbackEvaluator = null) {
|
||||||
|
$this->fallbackEvaluator = $fallbackEvaluator ?? function (Expr $expr) {
|
||||||
|
throw new ConstExprEvaluationException(
|
||||||
|
"Expression of type {$expr->getType()} cannot be evaluated"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Silently evaluates a constant expression into a PHP value.
|
||||||
|
*
|
||||||
|
* Thrown Errors, warnings or notices will be converted into a ConstExprEvaluationException.
|
||||||
|
* The original source of the exception is available through getPrevious().
|
||||||
|
*
|
||||||
|
* If some part of the expression cannot be evaluated, the fallback evaluator passed to the
|
||||||
|
* constructor will be invoked. By default, if no fallback is provided, an exception of type
|
||||||
|
* ConstExprEvaluationException is thrown.
|
||||||
|
*
|
||||||
|
* See class doc comment for caveats and limitations.
|
||||||
|
*
|
||||||
|
* @param Expr $expr Constant expression to evaluate
|
||||||
|
* @return mixed Result of evaluation
|
||||||
|
*
|
||||||
|
* @throws ConstExprEvaluationException if the expression cannot be evaluated or an error occurred
|
||||||
|
*/
|
||||||
|
public function evaluateSilently(Expr $expr) {
|
||||||
|
set_error_handler(function ($num, $str, $file, $line) {
|
||||||
|
throw new \ErrorException($str, 0, $num, $file, $line);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $this->evaluate($expr);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
if (!$e instanceof ConstExprEvaluationException) {
|
||||||
|
$e = new ConstExprEvaluationException(
|
||||||
|
"An error occurred during constant expression evaluation", 0, $e);
|
||||||
|
}
|
||||||
|
throw $e;
|
||||||
|
} finally {
|
||||||
|
restore_error_handler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directly evaluates a constant expression into a PHP value.
|
||||||
|
*
|
||||||
|
* May generate Error exceptions, warnings or notices. Use evaluateSilently() to convert these
|
||||||
|
* into a ConstExprEvaluationException.
|
||||||
|
*
|
||||||
|
* If some part of the expression cannot be evaluated, the fallback evaluator passed to the
|
||||||
|
* constructor will be invoked. By default, if no fallback is provided, an exception of type
|
||||||
|
* ConstExprEvaluationException is thrown.
|
||||||
|
*
|
||||||
|
* See class doc comment for caveats and limitations.
|
||||||
|
*
|
||||||
|
* @param Expr $expr Constant expression to evaluate
|
||||||
|
* @return mixed Result of evaluation
|
||||||
|
*
|
||||||
|
* @throws ConstExprEvaluationException if the expression cannot be evaluated
|
||||||
|
*/
|
||||||
|
public function evaluateDirectly(Expr $expr) {
|
||||||
|
return $this->evaluate($expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return mixed */
|
||||||
|
private function evaluate(Expr $expr) {
|
||||||
|
if ($expr instanceof Scalar\Int_
|
||||||
|
|| $expr instanceof Scalar\Float_
|
||||||
|
|| $expr instanceof Scalar\String_
|
||||||
|
) {
|
||||||
|
return $expr->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($expr instanceof Expr\Array_) {
|
||||||
|
return $this->evaluateArray($expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unary operators
|
||||||
|
if ($expr instanceof Expr\UnaryPlus) {
|
||||||
|
return +$this->evaluate($expr->expr);
|
||||||
|
}
|
||||||
|
if ($expr instanceof Expr\UnaryMinus) {
|
||||||
|
return -$this->evaluate($expr->expr);
|
||||||
|
}
|
||||||
|
if ($expr instanceof Expr\BooleanNot) {
|
||||||
|
return !$this->evaluate($expr->expr);
|
||||||
|
}
|
||||||
|
if ($expr instanceof Expr\BitwiseNot) {
|
||||||
|
return ~$this->evaluate($expr->expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($expr instanceof Expr\BinaryOp) {
|
||||||
|
return $this->evaluateBinaryOp($expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($expr instanceof Expr\Ternary) {
|
||||||
|
return $this->evaluateTernary($expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($expr instanceof Expr\ArrayDimFetch && null !== $expr->dim) {
|
||||||
|
return $this->evaluate($expr->var)[$this->evaluate($expr->dim)];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($expr instanceof Expr\ConstFetch) {
|
||||||
|
return $this->evaluateConstFetch($expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($this->fallbackEvaluator)($expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function evaluateArray(Expr\Array_ $expr): array {
|
||||||
|
$array = [];
|
||||||
|
foreach ($expr->items as $item) {
|
||||||
|
if (null !== $item->key) {
|
||||||
|
$array[$this->evaluate($item->key)] = $this->evaluate($item->value);
|
||||||
|
} elseif ($item->unpack) {
|
||||||
|
$array = array_merge($array, $this->evaluate($item->value));
|
||||||
|
} else {
|
||||||
|
$array[] = $this->evaluate($item->value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $array;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return mixed */
|
||||||
|
private function evaluateTernary(Expr\Ternary $expr) {
|
||||||
|
if (null === $expr->if) {
|
||||||
|
return $this->evaluate($expr->cond) ?: $this->evaluate($expr->else);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->evaluate($expr->cond)
|
||||||
|
? $this->evaluate($expr->if)
|
||||||
|
: $this->evaluate($expr->else);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return mixed */
|
||||||
|
private function evaluateBinaryOp(Expr\BinaryOp $expr) {
|
||||||
|
if ($expr instanceof Expr\BinaryOp\Coalesce
|
||||||
|
&& $expr->left instanceof Expr\ArrayDimFetch
|
||||||
|
) {
|
||||||
|
// This needs to be special cased to respect BP_VAR_IS fetch semantics
|
||||||
|
return $this->evaluate($expr->left->var)[$this->evaluate($expr->left->dim)]
|
||||||
|
?? $this->evaluate($expr->right);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The evaluate() calls are repeated in each branch, because some of the operators are
|
||||||
|
// short-circuiting and evaluating the RHS in advance may be illegal in that case
|
||||||
|
$l = $expr->left;
|
||||||
|
$r = $expr->right;
|
||||||
|
switch ($expr->getOperatorSigil()) {
|
||||||
|
case '&': return $this->evaluate($l) & $this->evaluate($r);
|
||||||
|
case '|': return $this->evaluate($l) | $this->evaluate($r);
|
||||||
|
case '^': return $this->evaluate($l) ^ $this->evaluate($r);
|
||||||
|
case '&&': return $this->evaluate($l) && $this->evaluate($r);
|
||||||
|
case '||': return $this->evaluate($l) || $this->evaluate($r);
|
||||||
|
case '??': return $this->evaluate($l) ?? $this->evaluate($r);
|
||||||
|
case '.': return $this->evaluate($l) . $this->evaluate($r);
|
||||||
|
case '/': return $this->evaluate($l) / $this->evaluate($r);
|
||||||
|
case '==': return $this->evaluate($l) == $this->evaluate($r);
|
||||||
|
case '>': return $this->evaluate($l) > $this->evaluate($r);
|
||||||
|
case '>=': return $this->evaluate($l) >= $this->evaluate($r);
|
||||||
|
case '===': return $this->evaluate($l) === $this->evaluate($r);
|
||||||
|
case 'and': return $this->evaluate($l) and $this->evaluate($r);
|
||||||
|
case 'or': return $this->evaluate($l) or $this->evaluate($r);
|
||||||
|
case 'xor': return $this->evaluate($l) xor $this->evaluate($r);
|
||||||
|
case '-': return $this->evaluate($l) - $this->evaluate($r);
|
||||||
|
case '%': return $this->evaluate($l) % $this->evaluate($r);
|
||||||
|
case '*': return $this->evaluate($l) * $this->evaluate($r);
|
||||||
|
case '!=': return $this->evaluate($l) != $this->evaluate($r);
|
||||||
|
case '!==': return $this->evaluate($l) !== $this->evaluate($r);
|
||||||
|
case '+': return $this->evaluate($l) + $this->evaluate($r);
|
||||||
|
case '**': return $this->evaluate($l) ** $this->evaluate($r);
|
||||||
|
case '<<': return $this->evaluate($l) << $this->evaluate($r);
|
||||||
|
case '>>': return $this->evaluate($l) >> $this->evaluate($r);
|
||||||
|
case '<': return $this->evaluate($l) < $this->evaluate($r);
|
||||||
|
case '<=': return $this->evaluate($l) <= $this->evaluate($r);
|
||||||
|
case '<=>': return $this->evaluate($l) <=> $this->evaluate($r);
|
||||||
|
case '|>':
|
||||||
|
$lval = $this->evaluate($l);
|
||||||
|
return $this->evaluate($r)($lval);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \Exception('Should not happen');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return mixed */
|
||||||
|
private function evaluateConstFetch(Expr\ConstFetch $expr) {
|
||||||
|
$name = $expr->name->toLowerString();
|
||||||
|
switch ($name) {
|
||||||
|
case 'null': return null;
|
||||||
|
case 'false': return false;
|
||||||
|
case 'true': return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($this->fallbackEvaluator)($expr);
|
||||||
|
}
|
||||||
|
}
|
||||||
+173
@@ -0,0 +1,173 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser;
|
||||||
|
|
||||||
|
class Error extends \RuntimeException {
|
||||||
|
protected string $rawMessage;
|
||||||
|
/** @var array<string, mixed> */
|
||||||
|
protected array $attributes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an Exception signifying a parse error.
|
||||||
|
*
|
||||||
|
* @param string $message Error message
|
||||||
|
* @param array<string, mixed> $attributes Attributes of node/token where error occurred
|
||||||
|
*/
|
||||||
|
public function __construct(string $message, array $attributes = []) {
|
||||||
|
$this->rawMessage = $message;
|
||||||
|
$this->attributes = $attributes;
|
||||||
|
$this->updateMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the error message
|
||||||
|
*
|
||||||
|
* @return string Error message
|
||||||
|
*/
|
||||||
|
public function getRawMessage(): string {
|
||||||
|
return $this->rawMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the line the error starts in.
|
||||||
|
*
|
||||||
|
* @return int Error start line
|
||||||
|
* @phpstan-return -1|positive-int
|
||||||
|
*/
|
||||||
|
public function getStartLine(): int {
|
||||||
|
return $this->attributes['startLine'] ?? -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the line the error ends in.
|
||||||
|
*
|
||||||
|
* @return int Error end line
|
||||||
|
* @phpstan-return -1|positive-int
|
||||||
|
*/
|
||||||
|
public function getEndLine(): int {
|
||||||
|
return $this->attributes['endLine'] ?? -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the attributes of the node/token the error occurred at.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function getAttributes(): array {
|
||||||
|
return $this->attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the attributes of the node/token the error occurred at.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $attributes
|
||||||
|
*/
|
||||||
|
public function setAttributes(array $attributes): void {
|
||||||
|
$this->attributes = $attributes;
|
||||||
|
$this->updateMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the line of the PHP file the error occurred in.
|
||||||
|
*
|
||||||
|
* @param string $message Error message
|
||||||
|
*/
|
||||||
|
public function setRawMessage(string $message): void {
|
||||||
|
$this->rawMessage = $message;
|
||||||
|
$this->updateMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the line the error starts in.
|
||||||
|
*
|
||||||
|
* @param int $line Error start line
|
||||||
|
*/
|
||||||
|
public function setStartLine(int $line): void {
|
||||||
|
$this->attributes['startLine'] = $line;
|
||||||
|
$this->updateMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the error has start and end column information.
|
||||||
|
*
|
||||||
|
* For column information enable the startFilePos and endFilePos in the lexer options.
|
||||||
|
*/
|
||||||
|
public function hasColumnInfo(): bool {
|
||||||
|
return isset($this->attributes['startFilePos'], $this->attributes['endFilePos']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the start column (1-based) into the line where the error started.
|
||||||
|
*
|
||||||
|
* @param string $code Source code of the file
|
||||||
|
*/
|
||||||
|
public function getStartColumn(string $code): int {
|
||||||
|
if (!$this->hasColumnInfo()) {
|
||||||
|
throw new \RuntimeException('Error does not have column information');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->toColumn($code, $this->attributes['startFilePos']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the end column (1-based) into the line where the error ended.
|
||||||
|
*
|
||||||
|
* @param string $code Source code of the file
|
||||||
|
*/
|
||||||
|
public function getEndColumn(string $code): int {
|
||||||
|
if (!$this->hasColumnInfo()) {
|
||||||
|
throw new \RuntimeException('Error does not have column information');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->toColumn($code, $this->attributes['endFilePos']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats message including line and column information.
|
||||||
|
*
|
||||||
|
* @param string $code Source code associated with the error, for calculation of the columns
|
||||||
|
*
|
||||||
|
* @return string Formatted message
|
||||||
|
*/
|
||||||
|
public function getMessageWithColumnInfo(string $code): string {
|
||||||
|
return sprintf(
|
||||||
|
'%s from %d:%d to %d:%d', $this->getRawMessage(),
|
||||||
|
$this->getStartLine(), $this->getStartColumn($code),
|
||||||
|
$this->getEndLine(), $this->getEndColumn($code)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a file offset into a column.
|
||||||
|
*
|
||||||
|
* @param string $code Source code that $pos indexes into
|
||||||
|
* @param int $pos 0-based position in $code
|
||||||
|
*
|
||||||
|
* @return int 1-based column (relative to start of line)
|
||||||
|
*/
|
||||||
|
private function toColumn(string $code, int $pos): int {
|
||||||
|
if ($pos > strlen($code)) {
|
||||||
|
throw new \RuntimeException('Invalid position information');
|
||||||
|
}
|
||||||
|
|
||||||
|
$lineStartPos = strrpos($code, "\n", $pos - strlen($code));
|
||||||
|
if (false === $lineStartPos) {
|
||||||
|
$lineStartPos = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $pos - $lineStartPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the exception message after a change to rawMessage or rawLine.
|
||||||
|
*/
|
||||||
|
protected function updateMessage(): void {
|
||||||
|
$this->message = $this->rawMessage;
|
||||||
|
|
||||||
|
if (-1 === $this->getStartLine()) {
|
||||||
|
$this->message .= ' on unknown line';
|
||||||
|
} else {
|
||||||
|
$this->message .= ' on line ' . $this->getStartLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser;
|
||||||
|
|
||||||
|
interface ErrorHandler {
|
||||||
|
/**
|
||||||
|
* Handle an error generated during lexing, parsing or some other operation.
|
||||||
|
*
|
||||||
|
* @param Error $error The error that needs to be handled
|
||||||
|
*/
|
||||||
|
public function handleError(Error $error): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\ErrorHandler;
|
||||||
|
|
||||||
|
use PhpParser\Error;
|
||||||
|
use PhpParser\ErrorHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error handler that collects all errors into an array.
|
||||||
|
*
|
||||||
|
* This allows graceful handling of errors.
|
||||||
|
*/
|
||||||
|
class Collecting implements ErrorHandler {
|
||||||
|
/** @var Error[] Collected errors */
|
||||||
|
private array $errors = [];
|
||||||
|
|
||||||
|
public function handleError(Error $error): void {
|
||||||
|
$this->errors[] = $error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get collected errors.
|
||||||
|
*
|
||||||
|
* @return Error[]
|
||||||
|
*/
|
||||||
|
public function getErrors(): array {
|
||||||
|
return $this->errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether there are any errors.
|
||||||
|
*/
|
||||||
|
public function hasErrors(): bool {
|
||||||
|
return !empty($this->errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset/clear collected errors.
|
||||||
|
*/
|
||||||
|
public function clearErrors(): void {
|
||||||
|
$this->errors = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\ErrorHandler;
|
||||||
|
|
||||||
|
use PhpParser\Error;
|
||||||
|
use PhpParser\ErrorHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error handler that handles all errors by throwing them.
|
||||||
|
*
|
||||||
|
* This is the default strategy used by all components.
|
||||||
|
*/
|
||||||
|
class Throwing implements ErrorHandler {
|
||||||
|
public function handleError(Error $error): void {
|
||||||
|
throw $error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Internal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class DiffElem {
|
||||||
|
public const TYPE_KEEP = 0;
|
||||||
|
public const TYPE_REMOVE = 1;
|
||||||
|
public const TYPE_ADD = 2;
|
||||||
|
public const TYPE_REPLACE = 3;
|
||||||
|
|
||||||
|
/** @var int One of the TYPE_* constants */
|
||||||
|
public int $type;
|
||||||
|
/** @var mixed Is null for add operations */
|
||||||
|
public $old;
|
||||||
|
/** @var mixed Is null for remove operations */
|
||||||
|
public $new;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $type One of the TYPE_* constants
|
||||||
|
* @param mixed $old Is null for add operations
|
||||||
|
* @param mixed $new Is null for remove operations
|
||||||
|
*/
|
||||||
|
public function __construct(int $type, $old, $new) {
|
||||||
|
$this->type = $type;
|
||||||
|
$this->old = $old;
|
||||||
|
$this->new = $new;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Internal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements the Myers diff algorithm.
|
||||||
|
*
|
||||||
|
* Myers, Eugene W. "An O (ND) difference algorithm and its variations."
|
||||||
|
* Algorithmica 1.1 (1986): 251-266.
|
||||||
|
*
|
||||||
|
* @template T
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class Differ {
|
||||||
|
/** @var callable(T, T): bool */
|
||||||
|
private $isEqual;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create differ over the given equality relation.
|
||||||
|
*
|
||||||
|
* @param callable(T, T): bool $isEqual Equality relation
|
||||||
|
*/
|
||||||
|
public function __construct(callable $isEqual) {
|
||||||
|
$this->isEqual = $isEqual;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate diff (edit script) from $old to $new.
|
||||||
|
*
|
||||||
|
* @param T[] $old Original array
|
||||||
|
* @param T[] $new New array
|
||||||
|
*
|
||||||
|
* @return DiffElem[] Diff (edit script)
|
||||||
|
*/
|
||||||
|
public function diff(array $old, array $new): array {
|
||||||
|
$old = \array_values($old);
|
||||||
|
$new = \array_values($new);
|
||||||
|
list($trace, $x, $y) = $this->calculateTrace($old, $new);
|
||||||
|
return $this->extractDiff($trace, $x, $y, $old, $new);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate diff, including "replace" operations.
|
||||||
|
*
|
||||||
|
* If a sequence of remove operations is followed by the same number of add operations, these
|
||||||
|
* will be coalesced into replace operations.
|
||||||
|
*
|
||||||
|
* @param T[] $old Original array
|
||||||
|
* @param T[] $new New array
|
||||||
|
*
|
||||||
|
* @return DiffElem[] Diff (edit script), including replace operations
|
||||||
|
*/
|
||||||
|
public function diffWithReplacements(array $old, array $new): array {
|
||||||
|
return $this->coalesceReplacements($this->diff($old, $new));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param T[] $old
|
||||||
|
* @param T[] $new
|
||||||
|
* @return array{array<int, array<int, int>>, int, int}
|
||||||
|
*/
|
||||||
|
private function calculateTrace(array $old, array $new): array {
|
||||||
|
$n = \count($old);
|
||||||
|
$m = \count($new);
|
||||||
|
$max = $n + $m;
|
||||||
|
$v = [1 => 0];
|
||||||
|
$trace = [];
|
||||||
|
for ($d = 0; $d <= $max; $d++) {
|
||||||
|
$trace[] = $v;
|
||||||
|
for ($k = -$d; $k <= $d; $k += 2) {
|
||||||
|
if ($k === -$d || ($k !== $d && $v[$k - 1] < $v[$k + 1])) {
|
||||||
|
$x = $v[$k + 1];
|
||||||
|
} else {
|
||||||
|
$x = $v[$k - 1] + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$y = $x - $k;
|
||||||
|
while ($x < $n && $y < $m && ($this->isEqual)($old[$x], $new[$y])) {
|
||||||
|
$x++;
|
||||||
|
$y++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$v[$k] = $x;
|
||||||
|
if ($x >= $n && $y >= $m) {
|
||||||
|
return [$trace, $x, $y];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new \Exception('Should not happen');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array<int, int>> $trace
|
||||||
|
* @param T[] $old
|
||||||
|
* @param T[] $new
|
||||||
|
* @return DiffElem[]
|
||||||
|
*/
|
||||||
|
private function extractDiff(array $trace, int $x, int $y, array $old, array $new): array {
|
||||||
|
$result = [];
|
||||||
|
for ($d = \count($trace) - 1; $d >= 0; $d--) {
|
||||||
|
$v = $trace[$d];
|
||||||
|
$k = $x - $y;
|
||||||
|
|
||||||
|
if ($k === -$d || ($k !== $d && $v[$k - 1] < $v[$k + 1])) {
|
||||||
|
$prevK = $k + 1;
|
||||||
|
} else {
|
||||||
|
$prevK = $k - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$prevX = $v[$prevK];
|
||||||
|
$prevY = $prevX - $prevK;
|
||||||
|
|
||||||
|
while ($x > $prevX && $y > $prevY) {
|
||||||
|
$result[] = new DiffElem(DiffElem::TYPE_KEEP, $old[$x - 1], $new[$y - 1]);
|
||||||
|
$x--;
|
||||||
|
$y--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($d === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
while ($x > $prevX) {
|
||||||
|
$result[] = new DiffElem(DiffElem::TYPE_REMOVE, $old[$x - 1], null);
|
||||||
|
$x--;
|
||||||
|
}
|
||||||
|
|
||||||
|
while ($y > $prevY) {
|
||||||
|
$result[] = new DiffElem(DiffElem::TYPE_ADD, null, $new[$y - 1]);
|
||||||
|
$y--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return array_reverse($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coalesce equal-length sequences of remove+add into a replace operation.
|
||||||
|
*
|
||||||
|
* @param DiffElem[] $diff
|
||||||
|
* @return DiffElem[]
|
||||||
|
*/
|
||||||
|
private function coalesceReplacements(array $diff): array {
|
||||||
|
$newDiff = [];
|
||||||
|
$c = \count($diff);
|
||||||
|
for ($i = 0; $i < $c; $i++) {
|
||||||
|
$diffType = $diff[$i]->type;
|
||||||
|
if ($diffType !== DiffElem::TYPE_REMOVE) {
|
||||||
|
$newDiff[] = $diff[$i];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$j = $i;
|
||||||
|
while ($j < $c && $diff[$j]->type === DiffElem::TYPE_REMOVE) {
|
||||||
|
$j++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$k = $j;
|
||||||
|
while ($k < $c && $diff[$k]->type === DiffElem::TYPE_ADD) {
|
||||||
|
$k++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($j - $i === $k - $j) {
|
||||||
|
$len = $j - $i;
|
||||||
|
for ($n = 0; $n < $len; $n++) {
|
||||||
|
$newDiff[] = new DiffElem(
|
||||||
|
DiffElem::TYPE_REPLACE, $diff[$i + $n]->old, $diff[$j + $n]->new
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (; $i < $k; $i++) {
|
||||||
|
$newDiff[] = $diff[$i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$i = $k - 1;
|
||||||
|
}
|
||||||
|
return $newDiff;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Internal;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This node is used internally by the format-preserving pretty printer to print anonymous classes.
|
||||||
|
*
|
||||||
|
* The normal anonymous class structure violates assumptions about the order of token offsets.
|
||||||
|
* Namely, the constructor arguments are part of the Expr\New_ node and follow the class node, even
|
||||||
|
* though they are actually interleaved with them. This special node type is used temporarily to
|
||||||
|
* restore a sane token offset order.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class PrintableNewAnonClassNode extends Expr {
|
||||||
|
/** @var Node\AttributeGroup[] PHP attribute groups */
|
||||||
|
public array $attrGroups;
|
||||||
|
/** @var int Modifiers */
|
||||||
|
public int $flags;
|
||||||
|
/** @var (Node\Arg|Node\VariadicPlaceholder)[] Arguments */
|
||||||
|
public array $args;
|
||||||
|
/** @var null|Node\Name Name of extended class */
|
||||||
|
public ?Node\Name $extends;
|
||||||
|
/** @var Node\Name[] Names of implemented interfaces */
|
||||||
|
public array $implements;
|
||||||
|
/** @var Node\Stmt[] Statements */
|
||||||
|
public array $stmts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Node\AttributeGroup[] $attrGroups PHP attribute groups
|
||||||
|
* @param (Node\Arg|Node\VariadicPlaceholder)[] $args Arguments
|
||||||
|
* @param Node\Name|null $extends Name of extended class
|
||||||
|
* @param Node\Name[] $implements Names of implemented interfaces
|
||||||
|
* @param Node\Stmt[] $stmts Statements
|
||||||
|
* @param array<string, mixed> $attributes Attributes
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
array $attrGroups, int $flags, array $args, ?Node\Name $extends, array $implements,
|
||||||
|
array $stmts, array $attributes
|
||||||
|
) {
|
||||||
|
parent::__construct($attributes);
|
||||||
|
$this->attrGroups = $attrGroups;
|
||||||
|
$this->flags = $flags;
|
||||||
|
$this->args = $args;
|
||||||
|
$this->extends = $extends;
|
||||||
|
$this->implements = $implements;
|
||||||
|
$this->stmts = $stmts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromNewNode(Expr\New_ $newNode): self {
|
||||||
|
$class = $newNode->class;
|
||||||
|
assert($class instanceof Node\Stmt\Class_);
|
||||||
|
// We don't assert that $class->name is null here, to allow consumers to assign unique names
|
||||||
|
// to anonymous classes for their own purposes. We simplify ignore the name here.
|
||||||
|
return new self(
|
||||||
|
$class->attrGroups, $class->flags, $newNode->args, $class->extends, $class->implements,
|
||||||
|
$class->stmts, $newNode->getAttributes()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getType(): string {
|
||||||
|
return 'Expr_PrintableNewAnonClass';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSubNodeNames(): array {
|
||||||
|
return ['attrGroups', 'flags', 'args', 'extends', 'implements', 'stmts'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Internal;
|
||||||
|
|
||||||
|
if (\PHP_VERSION_ID >= 80000) {
|
||||||
|
class TokenPolyfill extends \PhpToken {
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a polyfill for the PhpToken class introduced in PHP 8.0. We do not actually polyfill
|
||||||
|
* PhpToken, because composer might end up picking a different polyfill implementation, which does
|
||||||
|
* not meet our requirements.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class TokenPolyfill {
|
||||||
|
/** @var int The ID of the token. Either a T_* constant of a character code < 256. */
|
||||||
|
public int $id;
|
||||||
|
/** @var string The textual content of the token. */
|
||||||
|
public string $text;
|
||||||
|
/** @var int The 1-based starting line of the token (or -1 if unknown). */
|
||||||
|
public int $line;
|
||||||
|
/** @var int The 0-based starting position of the token (or -1 if unknown). */
|
||||||
|
public int $pos;
|
||||||
|
|
||||||
|
/** @var array<int, bool> Tokens ignored by the PHP parser. */
|
||||||
|
private const IGNORABLE_TOKENS = [
|
||||||
|
\T_WHITESPACE => true,
|
||||||
|
\T_COMMENT => true,
|
||||||
|
\T_DOC_COMMENT => true,
|
||||||
|
\T_OPEN_TAG => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @var array<int, bool> Tokens that may be part of a T_NAME_* identifier. */
|
||||||
|
private static array $identifierTokens;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Token with the given ID and text, as well optional line and position information.
|
||||||
|
*/
|
||||||
|
final public function __construct(int $id, string $text, int $line = -1, int $pos = -1) {
|
||||||
|
$this->id = $id;
|
||||||
|
$this->text = $text;
|
||||||
|
$this->line = $line;
|
||||||
|
$this->pos = $pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of the token. For single-char tokens this will be the token character.
|
||||||
|
* Otherwise it will be a T_* style name, or null if the token ID is unknown.
|
||||||
|
*/
|
||||||
|
public function getTokenName(): ?string {
|
||||||
|
if ($this->id < 256) {
|
||||||
|
return \chr($this->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = token_name($this->id);
|
||||||
|
return $name === 'UNKNOWN' ? null : $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the token is of the given kind. The kind may be either an integer that matches
|
||||||
|
* the token ID, a string that matches the token text, or an array of integers/strings. In the
|
||||||
|
* latter case, the function returns true if any of the kinds in the array match.
|
||||||
|
*
|
||||||
|
* @param int|string|(int|string)[] $kind
|
||||||
|
*/
|
||||||
|
public function is($kind): bool {
|
||||||
|
if (\is_int($kind)) {
|
||||||
|
return $this->id === $kind;
|
||||||
|
}
|
||||||
|
if (\is_string($kind)) {
|
||||||
|
return $this->text === $kind;
|
||||||
|
}
|
||||||
|
if (\is_array($kind)) {
|
||||||
|
foreach ($kind as $entry) {
|
||||||
|
if (\is_int($entry)) {
|
||||||
|
if ($this->id === $entry) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} elseif (\is_string($entry)) {
|
||||||
|
if ($this->text === $entry) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new \TypeError(
|
||||||
|
'Argument #1 ($kind) must only have elements of type string|int, ' .
|
||||||
|
gettype($entry) . ' given');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw new \TypeError(
|
||||||
|
'Argument #1 ($kind) must be of type string|int|array, ' .gettype($kind) . ' given');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether this token would be ignored by the PHP parser. Returns true for T_WHITESPACE,
|
||||||
|
* T_COMMENT, T_DOC_COMMENT and T_OPEN_TAG, and false for everything else.
|
||||||
|
*/
|
||||||
|
public function isIgnorable(): bool {
|
||||||
|
return isset(self::IGNORABLE_TOKENS[$this->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the textual content of the token.
|
||||||
|
*/
|
||||||
|
public function __toString(): string {
|
||||||
|
return $this->text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tokenize the given source code and return an array of tokens.
|
||||||
|
*
|
||||||
|
* This performs certain canonicalizations to match the PHP 8.0 token format:
|
||||||
|
* * Bad characters are represented using T_BAD_CHARACTER rather than omitted.
|
||||||
|
* * T_COMMENT does not include trailing newlines, instead the newline is part of a following
|
||||||
|
* T_WHITESPACE token.
|
||||||
|
* * Namespaced names are represented using T_NAME_* tokens.
|
||||||
|
*
|
||||||
|
* @return static[]
|
||||||
|
*/
|
||||||
|
public static function tokenize(string $code, int $flags = 0): array {
|
||||||
|
self::init();
|
||||||
|
|
||||||
|
$tokens = [];
|
||||||
|
$line = 1;
|
||||||
|
$pos = 0;
|
||||||
|
$origTokens = \token_get_all($code, $flags);
|
||||||
|
|
||||||
|
$numTokens = \count($origTokens);
|
||||||
|
for ($i = 0; $i < $numTokens; $i++) {
|
||||||
|
$token = $origTokens[$i];
|
||||||
|
if (\is_string($token)) {
|
||||||
|
if (\strlen($token) === 2) {
|
||||||
|
// b" and B" are tokenized as single-char tokens, even though they aren't.
|
||||||
|
$tokens[] = new static(\ord('"'), $token, $line, $pos);
|
||||||
|
$pos += 2;
|
||||||
|
} else {
|
||||||
|
$tokens[] = new static(\ord($token), $token, $line, $pos);
|
||||||
|
$pos++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$id = $token[0];
|
||||||
|
$text = $token[1];
|
||||||
|
|
||||||
|
// Emulate PHP 8.0 comment format, which does not include trailing whitespace anymore.
|
||||||
|
if ($id === \T_COMMENT && \substr($text, 0, 2) !== '/*' &&
|
||||||
|
\preg_match('/(\r\n|\n|\r)$/D', $text, $matches)
|
||||||
|
) {
|
||||||
|
$trailingNewline = $matches[0];
|
||||||
|
$text = \substr($text, 0, -\strlen($trailingNewline));
|
||||||
|
$tokens[] = new static($id, $text, $line, $pos);
|
||||||
|
$pos += \strlen($text);
|
||||||
|
|
||||||
|
if ($i + 1 < $numTokens && $origTokens[$i + 1][0] === \T_WHITESPACE) {
|
||||||
|
// Move trailing newline into following T_WHITESPACE token, if it already exists.
|
||||||
|
$origTokens[$i + 1][1] = $trailingNewline . $origTokens[$i + 1][1];
|
||||||
|
$origTokens[$i + 1][2]--;
|
||||||
|
} else {
|
||||||
|
// Otherwise, we need to create a new T_WHITESPACE token.
|
||||||
|
$tokens[] = new static(\T_WHITESPACE, $trailingNewline, $line, $pos);
|
||||||
|
$line++;
|
||||||
|
$pos += \strlen($trailingNewline);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emulate PHP 8.0 T_NAME_* tokens, by combining sequences of T_NS_SEPARATOR and
|
||||||
|
// T_STRING into a single token.
|
||||||
|
if (($id === \T_NS_SEPARATOR || isset(self::$identifierTokens[$id]))) {
|
||||||
|
$newText = $text;
|
||||||
|
$lastWasSeparator = $id === \T_NS_SEPARATOR;
|
||||||
|
for ($j = $i + 1; $j < $numTokens; $j++) {
|
||||||
|
if ($lastWasSeparator) {
|
||||||
|
if (!isset(self::$identifierTokens[$origTokens[$j][0]])) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$lastWasSeparator = false;
|
||||||
|
} else {
|
||||||
|
if ($origTokens[$j][0] !== \T_NS_SEPARATOR) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$lastWasSeparator = true;
|
||||||
|
}
|
||||||
|
$newText .= $origTokens[$j][1];
|
||||||
|
}
|
||||||
|
if ($lastWasSeparator) {
|
||||||
|
// Trailing separator is not part of the name.
|
||||||
|
$j--;
|
||||||
|
$newText = \substr($newText, 0, -1);
|
||||||
|
}
|
||||||
|
if ($j > $i + 1) {
|
||||||
|
if ($id === \T_NS_SEPARATOR) {
|
||||||
|
$id = \T_NAME_FULLY_QUALIFIED;
|
||||||
|
} elseif ($id === \T_NAMESPACE) {
|
||||||
|
$id = \T_NAME_RELATIVE;
|
||||||
|
} else {
|
||||||
|
$id = \T_NAME_QUALIFIED;
|
||||||
|
}
|
||||||
|
$tokens[] = new static($id, $newText, $line, $pos);
|
||||||
|
$pos += \strlen($newText);
|
||||||
|
$i = $j - 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$tokens[] = new static($id, $text, $line, $pos);
|
||||||
|
$line += \substr_count($text, "\n");
|
||||||
|
$pos += \strlen($text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Initialize private static state needed by tokenize(). */
|
||||||
|
private static function init(): void {
|
||||||
|
if (isset(self::$identifierTokens)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based on semi_reserved production.
|
||||||
|
self::$identifierTokens = \array_fill_keys([
|
||||||
|
\T_STRING,
|
||||||
|
\T_STATIC, \T_ABSTRACT, \T_FINAL, \T_PRIVATE, \T_PROTECTED, \T_PUBLIC, \T_READONLY,
|
||||||
|
\T_INCLUDE, \T_INCLUDE_ONCE, \T_EVAL, \T_REQUIRE, \T_REQUIRE_ONCE, \T_LOGICAL_OR, \T_LOGICAL_XOR, \T_LOGICAL_AND,
|
||||||
|
\T_INSTANCEOF, \T_NEW, \T_CLONE, \T_EXIT, \T_IF, \T_ELSEIF, \T_ELSE, \T_ENDIF, \T_ECHO, \T_DO, \T_WHILE,
|
||||||
|
\T_ENDWHILE, \T_FOR, \T_ENDFOR, \T_FOREACH, \T_ENDFOREACH, \T_DECLARE, \T_ENDDECLARE, \T_AS, \T_TRY, \T_CATCH,
|
||||||
|
\T_FINALLY, \T_THROW, \T_USE, \T_INSTEADOF, \T_GLOBAL, \T_VAR, \T_UNSET, \T_ISSET, \T_EMPTY, \T_CONTINUE, \T_GOTO,
|
||||||
|
\T_FUNCTION, \T_CONST, \T_RETURN, \T_PRINT, \T_YIELD, \T_LIST, \T_SWITCH, \T_ENDSWITCH, \T_CASE, \T_DEFAULT,
|
||||||
|
\T_BREAK, \T_ARRAY, \T_CALLABLE, \T_EXTENDS, \T_IMPLEMENTS, \T_NAMESPACE, \T_TRAIT, \T_INTERFACE, \T_CLASS,
|
||||||
|
\T_CLASS_C, \T_TRAIT_C, \T_FUNC_C, \T_METHOD_C, \T_LINE, \T_FILE, \T_DIR, \T_NS_C, \T_HALT_COMPILER, \T_FN,
|
||||||
|
\T_MATCH,
|
||||||
|
], true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Internal;
|
||||||
|
|
||||||
|
use PhpParser\Token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides operations on token streams, for use by pretty printer.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class TokenStream {
|
||||||
|
/** @var Token[] Tokens (in PhpToken::tokenize() format) */
|
||||||
|
private array $tokens;
|
||||||
|
/** @var int[] Map from position to indentation */
|
||||||
|
private array $indentMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create token stream instance.
|
||||||
|
*
|
||||||
|
* @param Token[] $tokens Tokens in PhpToken::tokenize() format
|
||||||
|
*/
|
||||||
|
public function __construct(array $tokens, int $tabWidth) {
|
||||||
|
$this->tokens = $tokens;
|
||||||
|
$this->indentMap = $this->calcIndentMap($tabWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the given position is immediately surrounded by parenthesis.
|
||||||
|
*
|
||||||
|
* @param int $startPos Start position
|
||||||
|
* @param int $endPos End position
|
||||||
|
*/
|
||||||
|
public function haveParens(int $startPos, int $endPos): bool {
|
||||||
|
return $this->haveTokenImmediatelyBefore($startPos, '(')
|
||||||
|
&& $this->haveTokenImmediatelyAfter($endPos, ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the given position is immediately surrounded by braces.
|
||||||
|
*
|
||||||
|
* @param int $startPos Start position
|
||||||
|
* @param int $endPos End position
|
||||||
|
*/
|
||||||
|
public function haveBraces(int $startPos, int $endPos): bool {
|
||||||
|
return ($this->haveTokenImmediatelyBefore($startPos, '{')
|
||||||
|
|| $this->haveTokenImmediatelyBefore($startPos, T_CURLY_OPEN))
|
||||||
|
&& $this->haveTokenImmediatelyAfter($endPos, '}');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the position is directly preceded by a certain token type.
|
||||||
|
*
|
||||||
|
* During this check whitespace and comments are skipped.
|
||||||
|
*
|
||||||
|
* @param int $pos Position before which the token should occur
|
||||||
|
* @param int|string $expectedTokenType Token to check for
|
||||||
|
*
|
||||||
|
* @return bool Whether the expected token was found
|
||||||
|
*/
|
||||||
|
public function haveTokenImmediatelyBefore(int $pos, $expectedTokenType): bool {
|
||||||
|
$tokens = $this->tokens;
|
||||||
|
$pos--;
|
||||||
|
for (; $pos >= 0; $pos--) {
|
||||||
|
$token = $tokens[$pos];
|
||||||
|
if ($token->is($expectedTokenType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!$token->isIgnorable()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the position is directly followed by a certain token type.
|
||||||
|
*
|
||||||
|
* During this check whitespace and comments are skipped.
|
||||||
|
*
|
||||||
|
* @param int $pos Position after which the token should occur
|
||||||
|
* @param int|string $expectedTokenType Token to check for
|
||||||
|
*
|
||||||
|
* @return bool Whether the expected token was found
|
||||||
|
*/
|
||||||
|
public function haveTokenImmediatelyAfter(int $pos, $expectedTokenType): bool {
|
||||||
|
$tokens = $this->tokens;
|
||||||
|
$pos++;
|
||||||
|
for ($c = \count($tokens); $pos < $c; $pos++) {
|
||||||
|
$token = $tokens[$pos];
|
||||||
|
if ($token->is($expectedTokenType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!$token->isIgnorable()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param int|string|(int|string)[] $skipTokenType */
|
||||||
|
public function skipLeft(int $pos, $skipTokenType): int {
|
||||||
|
$tokens = $this->tokens;
|
||||||
|
|
||||||
|
$pos = $this->skipLeftWhitespace($pos);
|
||||||
|
if ($skipTokenType === \T_WHITESPACE) {
|
||||||
|
return $pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$tokens[$pos]->is($skipTokenType)) {
|
||||||
|
// Shouldn't happen. The skip token MUST be there
|
||||||
|
throw new \Exception('Encountered unexpected token');
|
||||||
|
}
|
||||||
|
$pos--;
|
||||||
|
|
||||||
|
return $this->skipLeftWhitespace($pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param int|string|(int|string)[] $skipTokenType */
|
||||||
|
public function skipRight(int $pos, $skipTokenType): int {
|
||||||
|
$tokens = $this->tokens;
|
||||||
|
|
||||||
|
$pos = $this->skipRightWhitespace($pos);
|
||||||
|
if ($skipTokenType === \T_WHITESPACE) {
|
||||||
|
return $pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$tokens[$pos]->is($skipTokenType)) {
|
||||||
|
// Shouldn't happen. The skip token MUST be there
|
||||||
|
throw new \Exception('Encountered unexpected token');
|
||||||
|
}
|
||||||
|
$pos++;
|
||||||
|
|
||||||
|
return $this->skipRightWhitespace($pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return first non-whitespace token position smaller or equal to passed position.
|
||||||
|
*
|
||||||
|
* @param int $pos Token position
|
||||||
|
* @return int Non-whitespace token position
|
||||||
|
*/
|
||||||
|
public function skipLeftWhitespace(int $pos): int {
|
||||||
|
$tokens = $this->tokens;
|
||||||
|
for (; $pos >= 0; $pos--) {
|
||||||
|
if (!$tokens[$pos]->isIgnorable()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return first non-whitespace position greater or equal to passed position.
|
||||||
|
*
|
||||||
|
* @param int $pos Token position
|
||||||
|
* @return int Non-whitespace token position
|
||||||
|
*/
|
||||||
|
public function skipRightWhitespace(int $pos): int {
|
||||||
|
$tokens = $this->tokens;
|
||||||
|
for ($count = \count($tokens); $pos < $count; $pos++) {
|
||||||
|
if (!$tokens[$pos]->isIgnorable()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param int|string|(int|string)[] $findTokenType */
|
||||||
|
public function findRight(int $pos, $findTokenType): int {
|
||||||
|
$tokens = $this->tokens;
|
||||||
|
for ($count = \count($tokens); $pos < $count; $pos++) {
|
||||||
|
if ($tokens[$pos]->is($findTokenType)) {
|
||||||
|
return $pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the given position range contains a certain token type.
|
||||||
|
*
|
||||||
|
* @param int $startPos Starting position (inclusive)
|
||||||
|
* @param int $endPos Ending position (exclusive)
|
||||||
|
* @param int|string $tokenType Token type to look for
|
||||||
|
* @return bool Whether the token occurs in the given range
|
||||||
|
*/
|
||||||
|
public function haveTokenInRange(int $startPos, int $endPos, $tokenType): bool {
|
||||||
|
$tokens = $this->tokens;
|
||||||
|
for ($pos = $startPos; $pos < $endPos; $pos++) {
|
||||||
|
if ($tokens[$pos]->is($tokenType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function haveTagInRange(int $startPos, int $endPos): bool {
|
||||||
|
return $this->haveTokenInRange($startPos, $endPos, \T_OPEN_TAG)
|
||||||
|
|| $this->haveTokenInRange($startPos, $endPos, \T_CLOSE_TAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get indentation before token position.
|
||||||
|
*
|
||||||
|
* @param int $pos Token position
|
||||||
|
*
|
||||||
|
* @return int Indentation depth (in spaces)
|
||||||
|
*/
|
||||||
|
public function getIndentationBefore(int $pos): int {
|
||||||
|
return $this->indentMap[$pos];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the code corresponding to a token offset range, optionally adjusted for indentation.
|
||||||
|
*
|
||||||
|
* @param int $from Token start position (inclusive)
|
||||||
|
* @param int $to Token end position (exclusive)
|
||||||
|
* @param int $indent By how much the code should be indented (can be negative as well)
|
||||||
|
*
|
||||||
|
* @return string Code corresponding to token range, adjusted for indentation
|
||||||
|
*/
|
||||||
|
public function getTokenCode(int $from, int $to, int $indent): string {
|
||||||
|
$tokens = $this->tokens;
|
||||||
|
$result = '';
|
||||||
|
for ($pos = $from; $pos < $to; $pos++) {
|
||||||
|
$token = $tokens[$pos];
|
||||||
|
$id = $token->id;
|
||||||
|
$text = $token->text;
|
||||||
|
if ($id === \T_CONSTANT_ENCAPSED_STRING || $id === \T_ENCAPSED_AND_WHITESPACE) {
|
||||||
|
$result .= $text;
|
||||||
|
} else {
|
||||||
|
// TODO Handle non-space indentation
|
||||||
|
if ($indent < 0) {
|
||||||
|
$result .= str_replace("\n" . str_repeat(" ", -$indent), "\n", $text);
|
||||||
|
} elseif ($indent > 0) {
|
||||||
|
$result .= str_replace("\n", "\n" . str_repeat(" ", $indent), $text);
|
||||||
|
} else {
|
||||||
|
$result .= $text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Precalculate the indentation at every token position.
|
||||||
|
*
|
||||||
|
* @return int[] Token position to indentation map
|
||||||
|
*/
|
||||||
|
private function calcIndentMap(int $tabWidth): array {
|
||||||
|
$indentMap = [];
|
||||||
|
$indent = 0;
|
||||||
|
foreach ($this->tokens as $i => $token) {
|
||||||
|
$indentMap[] = $indent;
|
||||||
|
|
||||||
|
if ($token->id === \T_WHITESPACE) {
|
||||||
|
$content = $token->text;
|
||||||
|
$newlinePos = \strrpos($content, "\n");
|
||||||
|
if (false !== $newlinePos) {
|
||||||
|
$indent = $this->getIndent(\substr($content, $newlinePos + 1), $tabWidth);
|
||||||
|
} elseif ($i === 1 && $this->tokens[0]->id === \T_OPEN_TAG &&
|
||||||
|
$this->tokens[0]->text[\strlen($this->tokens[0]->text) - 1] === "\n") {
|
||||||
|
// Special case: Newline at the end of opening tag followed by whitespace.
|
||||||
|
$indent = $this->getIndent($content, $tabWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a sentinel for one past end of the file
|
||||||
|
$indentMap[] = $indent;
|
||||||
|
|
||||||
|
return $indentMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getIndent(string $ws, int $tabWidth): int {
|
||||||
|
$spaces = \substr_count($ws, " ");
|
||||||
|
$tabs = \substr_count($ws, "\t");
|
||||||
|
assert(\strlen($ws) === $spaces + $tabs);
|
||||||
|
return $spaces + $tabs * $tabWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser;
|
||||||
|
|
||||||
|
class JsonDecoder {
|
||||||
|
/** @var \ReflectionClass<Node>[] Node type to reflection class map */
|
||||||
|
private array $reflectionClassCache;
|
||||||
|
|
||||||
|
/** @return mixed */
|
||||||
|
public function decode(string $json) {
|
||||||
|
$value = json_decode($json, true);
|
||||||
|
if (json_last_error()) {
|
||||||
|
throw new \RuntimeException('JSON decoding error: ' . json_last_error_msg());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->decodeRecursive($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
private function decodeRecursive($value) {
|
||||||
|
if (\is_array($value)) {
|
||||||
|
if (isset($value['nodeType'])) {
|
||||||
|
if ($value['nodeType'] === 'Comment' || $value['nodeType'] === 'Comment_Doc') {
|
||||||
|
return $this->decodeComment($value);
|
||||||
|
}
|
||||||
|
return $this->decodeNode($value);
|
||||||
|
}
|
||||||
|
return $this->decodeArray($value);
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function decodeArray(array $array): array {
|
||||||
|
$decodedArray = [];
|
||||||
|
foreach ($array as $key => $value) {
|
||||||
|
$decodedArray[$key] = $this->decodeRecursive($value);
|
||||||
|
}
|
||||||
|
return $decodedArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function decodeNode(array $value): Node {
|
||||||
|
$nodeType = $value['nodeType'];
|
||||||
|
if (!\is_string($nodeType)) {
|
||||||
|
throw new \RuntimeException('Node type must be a string');
|
||||||
|
}
|
||||||
|
|
||||||
|
$reflectionClass = $this->reflectionClassFromNodeType($nodeType);
|
||||||
|
$node = $reflectionClass->newInstanceWithoutConstructor();
|
||||||
|
|
||||||
|
if (isset($value['attributes'])) {
|
||||||
|
if (!\is_array($value['attributes'])) {
|
||||||
|
throw new \RuntimeException('Attributes must be an array');
|
||||||
|
}
|
||||||
|
|
||||||
|
$node->setAttributes($this->decodeArray($value['attributes']));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($value as $name => $subNode) {
|
||||||
|
if ($name === 'nodeType' || $name === 'attributes') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$node->$name = $this->decodeRecursive($subNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function decodeComment(array $value): Comment {
|
||||||
|
$className = $value['nodeType'] === 'Comment' ? Comment::class : Comment\Doc::class;
|
||||||
|
if (!isset($value['text'])) {
|
||||||
|
throw new \RuntimeException('Comment must have text');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new $className(
|
||||||
|
$value['text'],
|
||||||
|
$value['line'] ?? -1, $value['filePos'] ?? -1, $value['tokenPos'] ?? -1,
|
||||||
|
$value['endLine'] ?? -1, $value['endFilePos'] ?? -1, $value['endTokenPos'] ?? -1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return \ReflectionClass<Node> */
|
||||||
|
private function reflectionClassFromNodeType(string $nodeType): \ReflectionClass {
|
||||||
|
if (!isset($this->reflectionClassCache[$nodeType])) {
|
||||||
|
$className = $this->classNameFromNodeType($nodeType);
|
||||||
|
$this->reflectionClassCache[$nodeType] = new \ReflectionClass($className);
|
||||||
|
}
|
||||||
|
return $this->reflectionClassCache[$nodeType];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return class-string<Node> */
|
||||||
|
private function classNameFromNodeType(string $nodeType): string {
|
||||||
|
$className = 'PhpParser\\Node\\' . strtr($nodeType, '_', '\\');
|
||||||
|
if (class_exists($className)) {
|
||||||
|
return $className;
|
||||||
|
}
|
||||||
|
|
||||||
|
$className .= '_';
|
||||||
|
if (class_exists($className)) {
|
||||||
|
return $className;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \RuntimeException("Unknown node type \"$nodeType\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser;
|
||||||
|
|
||||||
|
require __DIR__ . '/compatibility_tokens.php';
|
||||||
|
|
||||||
|
class Lexer {
|
||||||
|
/**
|
||||||
|
* Tokenize the provided source code.
|
||||||
|
*
|
||||||
|
* The token array is in the same format as provided by the PhpToken::tokenize() method in
|
||||||
|
* PHP 8.0. The tokens are instances of PhpParser\Token, to abstract over a polyfill
|
||||||
|
* implementation in earlier PHP version.
|
||||||
|
*
|
||||||
|
* The token array is terminated by a sentinel token with token ID 0.
|
||||||
|
* The token array does not discard any tokens (i.e. whitespace and comments are included).
|
||||||
|
* The token position attributes are against this token array.
|
||||||
|
*
|
||||||
|
* @param string $code The source code to tokenize.
|
||||||
|
* @param ErrorHandler|null $errorHandler Error handler to use for lexing errors. Defaults to
|
||||||
|
* ErrorHandler\Throwing.
|
||||||
|
* @return Token[] Tokens
|
||||||
|
*/
|
||||||
|
public function tokenize(string $code, ?ErrorHandler $errorHandler = null): array {
|
||||||
|
if (null === $errorHandler) {
|
||||||
|
$errorHandler = new ErrorHandler\Throwing();
|
||||||
|
}
|
||||||
|
|
||||||
|
$scream = ini_set('xdebug.scream', '0');
|
||||||
|
|
||||||
|
$tokens = @Token::tokenize($code);
|
||||||
|
$this->postprocessTokens($tokens, $errorHandler);
|
||||||
|
|
||||||
|
if (false !== $scream) {
|
||||||
|
ini_set('xdebug.scream', $scream);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleInvalidCharacter(Token $token, ErrorHandler $errorHandler): void {
|
||||||
|
$chr = $token->text;
|
||||||
|
if ($chr === "\0") {
|
||||||
|
// PHP cuts error message after null byte, so need special case
|
||||||
|
$errorMsg = 'Unexpected null byte';
|
||||||
|
} else {
|
||||||
|
$errorMsg = sprintf(
|
||||||
|
'Unexpected character "%s" (ASCII %d)', $chr, ord($chr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$errorHandler->handleError(new Error($errorMsg, [
|
||||||
|
'startLine' => $token->line,
|
||||||
|
'endLine' => $token->line,
|
||||||
|
'startFilePos' => $token->pos,
|
||||||
|
'endFilePos' => $token->pos,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isUnterminatedComment(Token $token): bool {
|
||||||
|
return $token->is([\T_COMMENT, \T_DOC_COMMENT])
|
||||||
|
&& substr($token->text, 0, 2) === '/*'
|
||||||
|
&& substr($token->text, -2) !== '*/';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<Token> $tokens
|
||||||
|
*/
|
||||||
|
protected function postprocessTokens(array &$tokens, ErrorHandler $errorHandler): void {
|
||||||
|
// This function reports errors (bad characters and unterminated comments) in the token
|
||||||
|
// array, and performs certain canonicalizations:
|
||||||
|
// * Use PHP 8.1 T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG and
|
||||||
|
// T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG tokens used to disambiguate intersection types.
|
||||||
|
// * Add a sentinel token with ID 0.
|
||||||
|
|
||||||
|
$numTokens = \count($tokens);
|
||||||
|
if ($numTokens === 0) {
|
||||||
|
// Empty input edge case: Just add the sentinel token.
|
||||||
|
$tokens[] = new Token(0, "\0", 1, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($i = 0; $i < $numTokens; $i++) {
|
||||||
|
$token = $tokens[$i];
|
||||||
|
if ($token->id === \T_BAD_CHARACTER) {
|
||||||
|
$this->handleInvalidCharacter($token, $errorHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($token->id === \ord('&')) {
|
||||||
|
$next = $i + 1;
|
||||||
|
while (isset($tokens[$next]) && $tokens[$next]->id === \T_WHITESPACE) {
|
||||||
|
$next++;
|
||||||
|
}
|
||||||
|
$followedByVarOrVarArg = isset($tokens[$next]) &&
|
||||||
|
$tokens[$next]->is([\T_VARIABLE, \T_ELLIPSIS]);
|
||||||
|
$token->id = $followedByVarOrVarArg
|
||||||
|
? \T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG
|
||||||
|
: \T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for unterminated comment
|
||||||
|
$lastToken = $tokens[$numTokens - 1];
|
||||||
|
if ($this->isUnterminatedComment($lastToken)) {
|
||||||
|
$errorHandler->handleError(new Error('Unterminated comment', [
|
||||||
|
'startLine' => $lastToken->line,
|
||||||
|
'endLine' => $lastToken->getEndLine(),
|
||||||
|
'startFilePos' => $lastToken->pos,
|
||||||
|
'endFilePos' => $lastToken->getEndPos(),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add sentinel token.
|
||||||
|
$tokens[] = new Token(0, "\0", $lastToken->getEndLine(), $lastToken->getEndPos());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Lexer;
|
||||||
|
|
||||||
|
use PhpParser\Error;
|
||||||
|
use PhpParser\ErrorHandler;
|
||||||
|
use PhpParser\Lexer;
|
||||||
|
use PhpParser\Lexer\TokenEmulator\AsymmetricVisibilityTokenEmulator;
|
||||||
|
use PhpParser\Lexer\TokenEmulator\AttributeEmulator;
|
||||||
|
use PhpParser\Lexer\TokenEmulator\EnumTokenEmulator;
|
||||||
|
use PhpParser\Lexer\TokenEmulator\ExplicitOctalEmulator;
|
||||||
|
use PhpParser\Lexer\TokenEmulator\MatchTokenEmulator;
|
||||||
|
use PhpParser\Lexer\TokenEmulator\NullsafeTokenEmulator;
|
||||||
|
use PhpParser\Lexer\TokenEmulator\PipeOperatorEmulator;
|
||||||
|
use PhpParser\Lexer\TokenEmulator\PropertyTokenEmulator;
|
||||||
|
use PhpParser\Lexer\TokenEmulator\ReadonlyFunctionTokenEmulator;
|
||||||
|
use PhpParser\Lexer\TokenEmulator\ReadonlyTokenEmulator;
|
||||||
|
use PhpParser\Lexer\TokenEmulator\ReverseEmulator;
|
||||||
|
use PhpParser\Lexer\TokenEmulator\TokenEmulator;
|
||||||
|
use PhpParser\Lexer\TokenEmulator\VoidCastEmulator;
|
||||||
|
use PhpParser\PhpVersion;
|
||||||
|
use PhpParser\Token;
|
||||||
|
|
||||||
|
class Emulative extends Lexer {
|
||||||
|
/** @var array{int, string, string}[] Patches used to reverse changes introduced in the code */
|
||||||
|
private array $patches = [];
|
||||||
|
|
||||||
|
/** @var list<TokenEmulator> */
|
||||||
|
private array $emulators = [];
|
||||||
|
|
||||||
|
private PhpVersion $targetPhpVersion;
|
||||||
|
|
||||||
|
private PhpVersion $hostPhpVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param PhpVersion|null $phpVersion PHP version to emulate. Defaults to newest supported.
|
||||||
|
*/
|
||||||
|
public function __construct(?PhpVersion $phpVersion = null) {
|
||||||
|
$this->targetPhpVersion = $phpVersion ?? PhpVersion::getNewestSupported();
|
||||||
|
$this->hostPhpVersion = PhpVersion::getHostVersion();
|
||||||
|
|
||||||
|
$emulators = [
|
||||||
|
new MatchTokenEmulator(),
|
||||||
|
new NullsafeTokenEmulator(),
|
||||||
|
new AttributeEmulator(),
|
||||||
|
new EnumTokenEmulator(),
|
||||||
|
new ReadonlyTokenEmulator(),
|
||||||
|
new ExplicitOctalEmulator(),
|
||||||
|
new ReadonlyFunctionTokenEmulator(),
|
||||||
|
new PropertyTokenEmulator(),
|
||||||
|
new AsymmetricVisibilityTokenEmulator(),
|
||||||
|
new PipeOperatorEmulator(),
|
||||||
|
new VoidCastEmulator(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Collect emulators that are relevant for the PHP version we're running
|
||||||
|
// and the PHP version we're targeting for emulation.
|
||||||
|
foreach ($emulators as $emulator) {
|
||||||
|
$emulatorPhpVersion = $emulator->getPhpVersion();
|
||||||
|
if ($this->isForwardEmulationNeeded($emulatorPhpVersion)) {
|
||||||
|
$this->emulators[] = $emulator;
|
||||||
|
} elseif ($this->isReverseEmulationNeeded($emulatorPhpVersion)) {
|
||||||
|
$this->emulators[] = new ReverseEmulator($emulator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tokenize(string $code, ?ErrorHandler $errorHandler = null): array {
|
||||||
|
$emulators = array_filter($this->emulators, function ($emulator) use ($code) {
|
||||||
|
return $emulator->isEmulationNeeded($code);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (empty($emulators)) {
|
||||||
|
// Nothing to emulate, yay
|
||||||
|
return parent::tokenize($code, $errorHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($errorHandler === null) {
|
||||||
|
$errorHandler = new ErrorHandler\Throwing();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->patches = [];
|
||||||
|
foreach ($emulators as $emulator) {
|
||||||
|
$code = $emulator->preprocessCode($code, $this->patches);
|
||||||
|
}
|
||||||
|
|
||||||
|
$collector = new ErrorHandler\Collecting();
|
||||||
|
$tokens = parent::tokenize($code, $collector);
|
||||||
|
$this->sortPatches();
|
||||||
|
$tokens = $this->fixupTokens($tokens);
|
||||||
|
|
||||||
|
$errors = $collector->getErrors();
|
||||||
|
if (!empty($errors)) {
|
||||||
|
$this->fixupErrors($errors);
|
||||||
|
foreach ($errors as $error) {
|
||||||
|
$errorHandler->handleError($error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($emulators as $emulator) {
|
||||||
|
$tokens = $emulator->emulate($code, $tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isForwardEmulationNeeded(PhpVersion $emulatorPhpVersion): bool {
|
||||||
|
return $this->hostPhpVersion->older($emulatorPhpVersion)
|
||||||
|
&& $this->targetPhpVersion->newerOrEqual($emulatorPhpVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isReverseEmulationNeeded(PhpVersion $emulatorPhpVersion): bool {
|
||||||
|
return $this->hostPhpVersion->newerOrEqual($emulatorPhpVersion)
|
||||||
|
&& $this->targetPhpVersion->older($emulatorPhpVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sortPatches(): void {
|
||||||
|
// Patches may be contributed by different emulators.
|
||||||
|
// Make sure they are sorted by increasing patch position.
|
||||||
|
usort($this->patches, function ($p1, $p2) {
|
||||||
|
return $p1[0] <=> $p2[0];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<Token> $tokens
|
||||||
|
* @return list<Token>
|
||||||
|
*/
|
||||||
|
private function fixupTokens(array $tokens): array {
|
||||||
|
if (\count($this->patches) === 0) {
|
||||||
|
return $tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load first patch
|
||||||
|
$patchIdx = 0;
|
||||||
|
list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx];
|
||||||
|
|
||||||
|
// We use a manual loop over the tokens, because we modify the array on the fly
|
||||||
|
$posDelta = 0;
|
||||||
|
$lineDelta = 0;
|
||||||
|
for ($i = 0, $c = \count($tokens); $i < $c; $i++) {
|
||||||
|
$token = $tokens[$i];
|
||||||
|
$pos = $token->pos;
|
||||||
|
$token->pos += $posDelta;
|
||||||
|
$token->line += $lineDelta;
|
||||||
|
$localPosDelta = 0;
|
||||||
|
$len = \strlen($token->text);
|
||||||
|
while ($patchPos >= $pos && $patchPos < $pos + $len) {
|
||||||
|
$patchTextLen = \strlen($patchText);
|
||||||
|
if ($patchType === 'remove') {
|
||||||
|
if ($patchPos === $pos && $patchTextLen === $len) {
|
||||||
|
// Remove token entirely
|
||||||
|
array_splice($tokens, $i, 1, []);
|
||||||
|
$i--;
|
||||||
|
$c--;
|
||||||
|
} else {
|
||||||
|
// Remove from token string
|
||||||
|
$token->text = substr_replace(
|
||||||
|
$token->text, '', $patchPos - $pos + $localPosDelta, $patchTextLen
|
||||||
|
);
|
||||||
|
$localPosDelta -= $patchTextLen;
|
||||||
|
}
|
||||||
|
$lineDelta -= \substr_count($patchText, "\n");
|
||||||
|
} elseif ($patchType === 'add') {
|
||||||
|
// Insert into the token string
|
||||||
|
$token->text = substr_replace(
|
||||||
|
$token->text, $patchText, $patchPos - $pos + $localPosDelta, 0
|
||||||
|
);
|
||||||
|
$localPosDelta += $patchTextLen;
|
||||||
|
$lineDelta += \substr_count($patchText, "\n");
|
||||||
|
} elseif ($patchType === 'replace') {
|
||||||
|
// Replace inside the token string
|
||||||
|
$token->text = substr_replace(
|
||||||
|
$token->text, $patchText, $patchPos - $pos + $localPosDelta, $patchTextLen
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
assert(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the next patch
|
||||||
|
$patchIdx++;
|
||||||
|
if ($patchIdx >= \count($this->patches)) {
|
||||||
|
// No more patches. However, we still need to adjust position.
|
||||||
|
$patchPos = \PHP_INT_MAX;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
list($patchPos, $patchType, $patchText) = $this->patches[$patchIdx];
|
||||||
|
}
|
||||||
|
|
||||||
|
$posDelta += $localPosDelta;
|
||||||
|
}
|
||||||
|
return $tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixup line and position information in errors.
|
||||||
|
*
|
||||||
|
* @param Error[] $errors
|
||||||
|
*/
|
||||||
|
private function fixupErrors(array $errors): void {
|
||||||
|
foreach ($errors as $error) {
|
||||||
|
$attrs = $error->getAttributes();
|
||||||
|
|
||||||
|
$posDelta = 0;
|
||||||
|
$lineDelta = 0;
|
||||||
|
foreach ($this->patches as $patch) {
|
||||||
|
list($patchPos, $patchType, $patchText) = $patch;
|
||||||
|
if ($patchPos >= $attrs['startFilePos']) {
|
||||||
|
// No longer relevant
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($patchType === 'add') {
|
||||||
|
$posDelta += strlen($patchText);
|
||||||
|
$lineDelta += substr_count($patchText, "\n");
|
||||||
|
} elseif ($patchType === 'remove') {
|
||||||
|
$posDelta -= strlen($patchText);
|
||||||
|
$lineDelta -= substr_count($patchText, "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$attrs['startFilePos'] += $posDelta;
|
||||||
|
$attrs['endFilePos'] += $posDelta;
|
||||||
|
$attrs['startLine'] += $lineDelta;
|
||||||
|
$attrs['endLine'] += $lineDelta;
|
||||||
|
$error->setAttributes($attrs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+93
@@ -0,0 +1,93 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Lexer\TokenEmulator;
|
||||||
|
|
||||||
|
use PhpParser\PhpVersion;
|
||||||
|
use PhpParser\Token;
|
||||||
|
|
||||||
|
final class AsymmetricVisibilityTokenEmulator extends TokenEmulator {
|
||||||
|
public function getPhpVersion(): PhpVersion {
|
||||||
|
return PhpVersion::fromComponents(8, 4);
|
||||||
|
}
|
||||||
|
public function isEmulationNeeded(string $code): bool {
|
||||||
|
$code = strtolower($code);
|
||||||
|
return strpos($code, 'public(set)') !== false ||
|
||||||
|
strpos($code, 'protected(set)') !== false ||
|
||||||
|
strpos($code, 'private(set)') !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function emulate(string $code, array $tokens): array {
|
||||||
|
$map = [
|
||||||
|
\T_PUBLIC => \T_PUBLIC_SET,
|
||||||
|
\T_PROTECTED => \T_PROTECTED_SET,
|
||||||
|
\T_PRIVATE => \T_PRIVATE_SET,
|
||||||
|
];
|
||||||
|
for ($i = 0, $c = count($tokens); $i < $c; ++$i) {
|
||||||
|
$token = $tokens[$i];
|
||||||
|
if (isset($map[$token->id]) && $i + 3 < $c && $tokens[$i + 1]->text === '(' &&
|
||||||
|
$tokens[$i + 2]->id === \T_STRING && \strtolower($tokens[$i + 2]->text) === 'set' &&
|
||||||
|
$tokens[$i + 3]->text === ')' &&
|
||||||
|
$this->isKeywordContext($tokens, $i)
|
||||||
|
) {
|
||||||
|
array_splice($tokens, $i, 4, [
|
||||||
|
new Token(
|
||||||
|
$map[$token->id], $token->text . '(' . $tokens[$i + 2]->text . ')',
|
||||||
|
$token->line, $token->pos),
|
||||||
|
]);
|
||||||
|
$c -= 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reverseEmulate(string $code, array $tokens): array {
|
||||||
|
$reverseMap = [
|
||||||
|
\T_PUBLIC_SET => \T_PUBLIC,
|
||||||
|
\T_PROTECTED_SET => \T_PROTECTED,
|
||||||
|
\T_PRIVATE_SET => \T_PRIVATE,
|
||||||
|
];
|
||||||
|
for ($i = 0, $c = count($tokens); $i < $c; ++$i) {
|
||||||
|
$token = $tokens[$i];
|
||||||
|
if (isset($reverseMap[$token->id]) &&
|
||||||
|
\preg_match('/(public|protected|private)\((set)\)/i', $token->text, $matches)
|
||||||
|
) {
|
||||||
|
[, $modifier, $set] = $matches;
|
||||||
|
$modifierLen = \strlen($modifier);
|
||||||
|
array_splice($tokens, $i, 1, [
|
||||||
|
new Token($reverseMap[$token->id], $modifier, $token->line, $token->pos),
|
||||||
|
new Token(\ord('('), '(', $token->line, $token->pos + $modifierLen),
|
||||||
|
new Token(\T_STRING, $set, $token->line, $token->pos + $modifierLen + 1),
|
||||||
|
new Token(\ord(')'), ')', $token->line, $token->pos + $modifierLen + 4),
|
||||||
|
]);
|
||||||
|
$i += 3;
|
||||||
|
$c += 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param Token[] $tokens */
|
||||||
|
protected function isKeywordContext(array $tokens, int $pos): bool {
|
||||||
|
$prevToken = $this->getPreviousNonSpaceToken($tokens, $pos);
|
||||||
|
if ($prevToken === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return $prevToken->id !== \T_OBJECT_OPERATOR
|
||||||
|
&& $prevToken->id !== \T_NULLSAFE_OBJECT_OPERATOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param Token[] $tokens */
|
||||||
|
private function getPreviousNonSpaceToken(array $tokens, int $start): ?Token {
|
||||||
|
for ($i = $start - 1; $i >= 0; --$i) {
|
||||||
|
if ($tokens[$i]->id === T_WHITESPACE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tokens[$i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Lexer\TokenEmulator;
|
||||||
|
|
||||||
|
use PhpParser\PhpVersion;
|
||||||
|
use PhpParser\Token;
|
||||||
|
|
||||||
|
final class AttributeEmulator extends TokenEmulator {
|
||||||
|
public function getPhpVersion(): PhpVersion {
|
||||||
|
return PhpVersion::fromComponents(8, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEmulationNeeded(string $code): bool {
|
||||||
|
return strpos($code, '#[') !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function emulate(string $code, array $tokens): array {
|
||||||
|
// We need to manually iterate and manage a count because we'll change
|
||||||
|
// the tokens array on the way.
|
||||||
|
for ($i = 0, $c = count($tokens); $i < $c; ++$i) {
|
||||||
|
$token = $tokens[$i];
|
||||||
|
if ($token->text === '#' && isset($tokens[$i + 1]) && $tokens[$i + 1]->text === '[') {
|
||||||
|
array_splice($tokens, $i, 2, [
|
||||||
|
new Token(\T_ATTRIBUTE, '#[', $token->line, $token->pos),
|
||||||
|
]);
|
||||||
|
$c--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reverseEmulate(string $code, array $tokens): array {
|
||||||
|
// TODO
|
||||||
|
return $tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function preprocessCode(string $code, array &$patches): string {
|
||||||
|
$pos = 0;
|
||||||
|
while (false !== $pos = strpos($code, '#[', $pos)) {
|
||||||
|
// Replace #[ with %[
|
||||||
|
$code[$pos] = '%';
|
||||||
|
$patches[] = [$pos, 'replace', '#'];
|
||||||
|
$pos += 2;
|
||||||
|
}
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Lexer\TokenEmulator;
|
||||||
|
|
||||||
|
use PhpParser\PhpVersion;
|
||||||
|
|
||||||
|
final class EnumTokenEmulator extends KeywordEmulator {
|
||||||
|
public function getPhpVersion(): PhpVersion {
|
||||||
|
return PhpVersion::fromComponents(8, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKeywordString(): string {
|
||||||
|
return 'enum';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKeywordToken(): int {
|
||||||
|
return \T_ENUM;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isKeywordContext(array $tokens, int $pos): bool {
|
||||||
|
return parent::isKeywordContext($tokens, $pos)
|
||||||
|
&& isset($tokens[$pos + 2])
|
||||||
|
&& $tokens[$pos + 1]->id === \T_WHITESPACE
|
||||||
|
&& $tokens[$pos + 2]->id === \T_STRING;
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Lexer\TokenEmulator;
|
||||||
|
|
||||||
|
use PhpParser\PhpVersion;
|
||||||
|
use PhpParser\Token;
|
||||||
|
|
||||||
|
class ExplicitOctalEmulator extends TokenEmulator {
|
||||||
|
public function getPhpVersion(): PhpVersion {
|
||||||
|
return PhpVersion::fromComponents(8, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEmulationNeeded(string $code): bool {
|
||||||
|
return strpos($code, '0o') !== false || strpos($code, '0O') !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function emulate(string $code, array $tokens): array {
|
||||||
|
for ($i = 0, $c = count($tokens); $i < $c; ++$i) {
|
||||||
|
$token = $tokens[$i];
|
||||||
|
if ($token->id == \T_LNUMBER && $token->text === '0' &&
|
||||||
|
isset($tokens[$i + 1]) && $tokens[$i + 1]->id == \T_STRING &&
|
||||||
|
preg_match('/[oO][0-7]+(?:_[0-7]+)*/', $tokens[$i + 1]->text)
|
||||||
|
) {
|
||||||
|
$tokenKind = $this->resolveIntegerOrFloatToken($tokens[$i + 1]->text);
|
||||||
|
array_splice($tokens, $i, 2, [
|
||||||
|
new Token($tokenKind, '0' . $tokens[$i + 1]->text, $token->line, $token->pos),
|
||||||
|
]);
|
||||||
|
$c--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveIntegerOrFloatToken(string $str): int {
|
||||||
|
$str = substr($str, 1);
|
||||||
|
$str = str_replace('_', '', $str);
|
||||||
|
$num = octdec($str);
|
||||||
|
return is_float($num) ? \T_DNUMBER : \T_LNUMBER;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reverseEmulate(string $code, array $tokens): array {
|
||||||
|
// Explicit octals were not legal code previously, don't bother.
|
||||||
|
return $tokens;
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Lexer\TokenEmulator;
|
||||||
|
|
||||||
|
use PhpParser\Token;
|
||||||
|
|
||||||
|
abstract class KeywordEmulator extends TokenEmulator {
|
||||||
|
abstract public function getKeywordString(): string;
|
||||||
|
abstract public function getKeywordToken(): int;
|
||||||
|
|
||||||
|
public function isEmulationNeeded(string $code): bool {
|
||||||
|
return strpos(strtolower($code), $this->getKeywordString()) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param Token[] $tokens */
|
||||||
|
protected function isKeywordContext(array $tokens, int $pos): bool {
|
||||||
|
$prevToken = $this->getPreviousNonSpaceToken($tokens, $pos);
|
||||||
|
if ($prevToken === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return $prevToken->id !== \T_OBJECT_OPERATOR
|
||||||
|
&& $prevToken->id !== \T_NULLSAFE_OBJECT_OPERATOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function emulate(string $code, array $tokens): array {
|
||||||
|
$keywordString = $this->getKeywordString();
|
||||||
|
foreach ($tokens as $i => $token) {
|
||||||
|
if ($token->id === T_STRING && strtolower($token->text) === $keywordString
|
||||||
|
&& $this->isKeywordContext($tokens, $i)) {
|
||||||
|
$token->id = $this->getKeywordToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param Token[] $tokens */
|
||||||
|
private function getPreviousNonSpaceToken(array $tokens, int $start): ?Token {
|
||||||
|
for ($i = $start - 1; $i >= 0; --$i) {
|
||||||
|
if ($tokens[$i]->id === T_WHITESPACE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tokens[$i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reverseEmulate(string $code, array $tokens): array {
|
||||||
|
$keywordToken = $this->getKeywordToken();
|
||||||
|
foreach ($tokens as $token) {
|
||||||
|
if ($token->id === $keywordToken) {
|
||||||
|
$token->id = \T_STRING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tokens;
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Lexer\TokenEmulator;
|
||||||
|
|
||||||
|
use PhpParser\PhpVersion;
|
||||||
|
|
||||||
|
final class MatchTokenEmulator extends KeywordEmulator {
|
||||||
|
public function getPhpVersion(): PhpVersion {
|
||||||
|
return PhpVersion::fromComponents(8, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKeywordString(): string {
|
||||||
|
return 'match';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKeywordToken(): int {
|
||||||
|
return \T_MATCH;
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Lexer\TokenEmulator;
|
||||||
|
|
||||||
|
use PhpParser\PhpVersion;
|
||||||
|
use PhpParser\Token;
|
||||||
|
|
||||||
|
final class NullsafeTokenEmulator extends TokenEmulator {
|
||||||
|
public function getPhpVersion(): PhpVersion {
|
||||||
|
return PhpVersion::fromComponents(8, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEmulationNeeded(string $code): bool {
|
||||||
|
return strpos($code, '?->') !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function emulate(string $code, array $tokens): array {
|
||||||
|
// We need to manually iterate and manage a count because we'll change
|
||||||
|
// the tokens array on the way
|
||||||
|
for ($i = 0, $c = count($tokens); $i < $c; ++$i) {
|
||||||
|
$token = $tokens[$i];
|
||||||
|
if ($token->text === '?' && isset($tokens[$i + 1]) && $tokens[$i + 1]->id === \T_OBJECT_OPERATOR) {
|
||||||
|
array_splice($tokens, $i, 2, [
|
||||||
|
new Token(\T_NULLSAFE_OBJECT_OPERATOR, '?->', $token->line, $token->pos),
|
||||||
|
]);
|
||||||
|
$c--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ?-> inside encapsed string.
|
||||||
|
if ($token->id === \T_ENCAPSED_AND_WHITESPACE && isset($tokens[$i - 1])
|
||||||
|
&& $tokens[$i - 1]->id === \T_VARIABLE
|
||||||
|
&& preg_match('/^\?->([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)/', $token->text, $matches)
|
||||||
|
) {
|
||||||
|
$replacement = [
|
||||||
|
new Token(\T_NULLSAFE_OBJECT_OPERATOR, '?->', $token->line, $token->pos),
|
||||||
|
new Token(\T_STRING, $matches[1], $token->line, $token->pos + 3),
|
||||||
|
];
|
||||||
|
$matchLen = \strlen($matches[0]);
|
||||||
|
if ($matchLen !== \strlen($token->text)) {
|
||||||
|
$replacement[] = new Token(
|
||||||
|
\T_ENCAPSED_AND_WHITESPACE,
|
||||||
|
\substr($token->text, $matchLen),
|
||||||
|
$token->line, $token->pos + $matchLen
|
||||||
|
);
|
||||||
|
}
|
||||||
|
array_splice($tokens, $i, 1, $replacement);
|
||||||
|
$c += \count($replacement) - 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reverseEmulate(string $code, array $tokens): array {
|
||||||
|
// ?-> was not valid code previously, don't bother.
|
||||||
|
return $tokens;
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Lexer\TokenEmulator;
|
||||||
|
|
||||||
|
use PhpParser\Lexer\TokenEmulator\TokenEmulator;
|
||||||
|
use PhpParser\PhpVersion;
|
||||||
|
use PhpParser\Token;
|
||||||
|
|
||||||
|
class PipeOperatorEmulator extends TokenEmulator {
|
||||||
|
public function getPhpVersion(): PhpVersion {
|
||||||
|
return PhpVersion::fromComponents(8, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEmulationNeeded(string $code): bool {
|
||||||
|
return \strpos($code, '|>') !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function emulate(string $code, array $tokens): array {
|
||||||
|
for ($i = 0, $c = count($tokens); $i < $c; ++$i) {
|
||||||
|
$token = $tokens[$i];
|
||||||
|
if ($token->text === '|' && isset($tokens[$i + 1]) && $tokens[$i + 1]->text === '>') {
|
||||||
|
array_splice($tokens, $i, 2, [
|
||||||
|
new Token(\T_PIPE, '|>', $token->line, $token->pos),
|
||||||
|
]);
|
||||||
|
$c--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reverseEmulate(string $code, array $tokens): array {
|
||||||
|
for ($i = 0, $c = count($tokens); $i < $c; ++$i) {
|
||||||
|
$token = $tokens[$i];
|
||||||
|
if ($token->id === \T_PIPE) {
|
||||||
|
array_splice($tokens, $i, 1, [
|
||||||
|
new Token(\ord('|'), '|', $token->line, $token->pos),
|
||||||
|
new Token(\ord('>'), '>', $token->line, $token->pos + 1),
|
||||||
|
]);
|
||||||
|
$i++;
|
||||||
|
$c++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $tokens;
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Lexer\TokenEmulator;
|
||||||
|
|
||||||
|
use PhpParser\PhpVersion;
|
||||||
|
|
||||||
|
final class PropertyTokenEmulator extends KeywordEmulator {
|
||||||
|
public function getPhpVersion(): PhpVersion {
|
||||||
|
return PhpVersion::fromComponents(8, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKeywordString(): string {
|
||||||
|
return '__property__';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKeywordToken(): int {
|
||||||
|
return \T_PROPERTY_C;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+31
@@ -0,0 +1,31 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Lexer\TokenEmulator;
|
||||||
|
|
||||||
|
use PhpParser\PhpVersion;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* In PHP 8.1, "readonly(" was special cased in the lexer in order to support functions with
|
||||||
|
* name readonly. In PHP 8.2, this may conflict with readonly properties having a DNF type. For
|
||||||
|
* this reason, PHP 8.2 instead treats this as T_READONLY and then handles it specially in the
|
||||||
|
* parser. This emulator only exists to handle this special case, which is skipped by the
|
||||||
|
* PHP 8.1 ReadonlyTokenEmulator.
|
||||||
|
*/
|
||||||
|
class ReadonlyFunctionTokenEmulator extends KeywordEmulator {
|
||||||
|
public function getKeywordString(): string {
|
||||||
|
return 'readonly';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKeywordToken(): int {
|
||||||
|
return \T_READONLY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPhpVersion(): PhpVersion {
|
||||||
|
return PhpVersion::fromComponents(8, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reverseEmulate(string $code, array $tokens): array {
|
||||||
|
// Don't bother
|
||||||
|
return $tokens;
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Lexer\TokenEmulator;
|
||||||
|
|
||||||
|
use PhpParser\PhpVersion;
|
||||||
|
|
||||||
|
final class ReadonlyTokenEmulator extends KeywordEmulator {
|
||||||
|
public function getPhpVersion(): PhpVersion {
|
||||||
|
return PhpVersion::fromComponents(8, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKeywordString(): string {
|
||||||
|
return 'readonly';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKeywordToken(): int {
|
||||||
|
return \T_READONLY;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isKeywordContext(array $tokens, int $pos): bool {
|
||||||
|
if (!parent::isKeywordContext($tokens, $pos)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Support "function readonly("
|
||||||
|
return !(isset($tokens[$pos + 1]) &&
|
||||||
|
($tokens[$pos + 1]->text === '(' ||
|
||||||
|
($tokens[$pos + 1]->id === \T_WHITESPACE &&
|
||||||
|
isset($tokens[$pos + 2]) &&
|
||||||
|
$tokens[$pos + 2]->text === '(')));
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace PhpParser\Lexer\TokenEmulator;
|
||||||
|
|
||||||
|
use PhpParser\PhpVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverses emulation direction of the inner emulator.
|
||||||
|
*/
|
||||||
|
final class ReverseEmulator extends TokenEmulator {
|
||||||
|
/** @var TokenEmulator Inner emulator */
|
||||||
|
private TokenEmulator $emulator;
|
||||||
|
|
||||||
|
public function __construct(TokenEmulator $emulator) {
|
||||||
|
$this->emulator = $emulator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPhpVersion(): PhpVersion {
|
||||||
|
return $this->emulator->getPhpVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEmulationNeeded(string $code): bool {
|
||||||
|
return $this->emulator->isEmulationNeeded($code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function emulate(string $code, array $tokens): array {
|
||||||
|
return $this->emulator->reverseEmulate($code, $tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reverseEmulate(string $code, array $tokens): array {
|
||||||
|
return $this->emulator->emulate($code, $tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function preprocessCode(string $code, array &$patches): string {
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user