* @git Joomla Component Builder * @copyright Copyright (C) 2015 Vast Development Method. All rights reserved. * @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace VDM\Joomla\Componentbuilder\Compiler\Power; use VDM\Joomla\Componentbuilder\Compiler\Factory as Compiler; use VDM\Joomla\Componentbuilder\Compiler\Power; use VDM\Joomla\Componentbuilder\Compiler\Power\Extractor; use VDM\Joomla\Componentbuilder\Compiler\Power\Parser; use VDM\Joomla\Componentbuilder\Compiler\Placeholder; /** * Compiler Power Injector * @since 3.2.0 */ final class Injector { /** * Power Objects * * @var Power * @since 3.2.0 **/ protected Power $power; /** * Compiler Powers Extractor * * @var Extractor * @since 3.2.0 **/ protected Extractor $extractor; /** * Compiler Powers Parser * * @var Parser * @since 3.2.0 **/ protected Parser $parser; /** * Compiler Placeholder * * @var Placeholder * @since 3.2.0 */ protected Placeholder $placeholder; /** * Super Power Update Map * * @var array * @since 3.2.0 */ protected array $map = []; /** * Insert Use Statements * * @var array * @since 3.2.0 */ protected array $useStatements = []; /** * Insert Trait Statements * * @var array * @since 3.2.0 */ protected array $traits = []; /** * Other Statements * * @var array * @since 3.2.0 */ protected array $other = []; /** * Duplicate Statements * * @var array * @since 3.2.0 */ protected array $duplicate = []; /** * Constructor. * * @param Power|null $power The power object. * @param Extractor|null $extractor The powers extractor object. * @param Parser|null $parser The powers parser object. * @param Placeholder|null $placeholder The compiler placeholder object. * * @since 3.2.0 */ public function __construct(?Power $power = null, ?Extractor $extractor = null, ?Parser $parser = null, ?Placeholder $placeholder = null) { $this->power = $power ?: Compiler::_('Power'); $this->extractor = $extractor ?: Compiler::_('Power.Extractor'); $this->parser = $parser ?: Compiler::_('Power.Parser'); $this->placeholder = $placeholder ?: Compiler::_('Placeholder'); } /** * Inject the powers found in the code * * @param string $code The class code * * @return string The updated code * @since 3.2.0 */ public function power(string $code): string { if (($guids = $this->extractor->get($code)) !== null) { return $this->update($guids, $code); } return $code; } /** * Update the code * * @param array $guids The Power guids found * @param string $code The class code * * @return string The updated code * @since 3.2.0 */ protected function update(array $guids, string $code): string { $use_statements = $this->parser->getUseStatements($code); $traits = $this->parser->getTraits( $this->parser->getClassCode($code) ?? '' ); // reset with each file $this->map = []; $this->useStatements = []; $this->traits = []; $this->other = []; $this->duplicate = []; foreach ($guids as $key => $guid) { if (($power = $this->power->get($guid)) !== null) { if (($name = $this->inspect($power, $use_statements, $traits)) !== null) { $this->map[$key] = $name; } } } // update if ($this->map !== []) { if ($this->useStatements !== []) { $code = $this->addUseStatements($code, $use_statements); } return $this->placeholder->update($code, $this->map); } return $code; } /** * Inspect the super power to determine the necessary class name based on use statements and traits. * It checks if the given power (class, trait, etc.) already has a corresponding use statement * and handles the naming accordingly to avoid conflicts. * * @param object $power The power object containing type, namespace, and class name. * @param array|null $useStatements Array of existing use statements in the code. * @param array|null $traits Array of existing traits used in the code. * * @return string|null The determined class name, or null if the type is not valid. * @since 3.2.0 */ protected function inspect(object $power, ?array $useStatements, ?array $traits): ?string { $namespaceStatement = $this->buildNamespaceStatment($power); $use_extracted = $this->extractUseStatements($namespaceStatement, $power->class_name, $useStatements); $name = $use_extracted['found'] ?? $power->class_name; $name = $this->getUniqueName($name, $power); $this->handleTraitLogic($name, $power, $traits); if (!$use_extracted['hasStatement']) { $this->addUseStatement($name, $power->class_name, $namespaceStatement); } return $name; } /** * Builds the namespace statement from the power object's namespace and class name. * * @param object $power The power object. * * @return string The constructed use statement. * @since 3.2.0 */ protected function buildNamespaceStatment(object $power): string { return $power->_namespace . '\\' . $power->class_name; } /** * Extracts and processes use statements to find if the current class name is already used. * It identifies any potential naming conflicts. * * @param string $useStatement The search statement of the current class. * @param string $className The class name of the power object. * @param array|null $useStatements The existing use statements. * * @return array An array with keys 'found' and 'hasStatement'. * @since 3.2.0 */ protected function extractUseStatements(string $useStatement, string $className, ?array $useStatements): array { $results = ['found' => null, 'hasStatement' => false]; if ($useStatements !== null) { foreach ($useStatements as $use_statement) { $class_name = $this->extractClassNameOrAlias($use_statement); if ($this->isUseStatementEqual($use_statement, $useStatement)) { if ($results['found'] === null) { $results['found'] = $class_name; $results['hasStatement'] = true; } else { // TODO we need to backport fix these $this->duplicate[$use_statement] = $class_name; } } elseif ($className === $class_name) { $this->other[$className] = $class_name; } } } return $results; } /** * Checks if the namespace statement is already declared in the current use statements. * * This method uses a regular expression to check for an exact match of the full statement, * taking into account the possibility of an alias being used. * * @param string $useStatement The existing use statement to check against. * @param string $namespaceStatement The search statement to search for (without the trailing semicolon, or use prefix). * * @return bool True if the full statement is found, false otherwise. */ protected function isUseStatementEqual(string $useStatement, string $namespaceStatement): bool { // Create a regular expression pattern to match the full statement // The pattern checks for the start of the statement, optional whitespace, // and an optional alias after the full statement. $pattern = '/^use\s+' . preg_quote($namespaceStatement, '/') . '(?:\s+as\s+\w+)?;$/'; // Perform the regex match to check if the use statement is equal to the search statment return (bool) preg_match($pattern, $useStatement); } /** * Extracts the class name or alias from a use statement. * * This method parses a PHP 'use' statement and extracts either the class name or its alias. * If the statement doesn't match the expected format, or if no class name or alias is found, * the method returns null. * * Example: * - 'use Namespace\ClassName;' -> returns 'ClassName' * - 'use Namespace\ClassName as Alias;' -> returns 'Alias' * * @param string $useStatement The use statement from which to extract the class name or alias. * * @return string|null The class name or alias if found, null otherwise. * @since 3.2.0 */ protected function extractClassNameOrAlias(string $useStatement): ?string { // If the input doesn't start with 'use ', assume it's just the namespace without a use statement if (strpos($useStatement, 'use ') !== 0) { return $this->extractLastNameFromNamespace($useStatement); } // Regular expression to extract the class name and alias from the use statement $pattern = '/use\s+(?P[\w\\\\]+?)(?:\s+as\s+(?P\w+))?;/'; if (preg_match($pattern, $useStatement, $matches)) { // Return the alias if it exists; otherwise, return the last part of the namespace (class name) return $matches['alias'] ?? $this->extractLastNameFromNamespace($matches['namespace']); } // Return null if no match is found return null; } /** * Ensures the name for the use statement is unique, avoiding conflicts with other classes. * * @param string $name The current name * @param object $power The power object containing type, namespace, and class name. * * @return string The unique name * @since 3.2.0 */ protected function getUniqueName(string $name, object $power): string { // set search namespace $namespace = ($name !== $power->class_name) ? $this->buildNamespaceStatment($power) : $power->_namespace; // check if we need to update the name if (isset($this->other[$name])) { // if the name is already used while (isset($this->other[$name])) { if (($tmp = $this->extractClassNameOrAlias($namespace)) !== null) { $name = ucfirst($tmp) . $name; $namespace = $this->removeLastNameFromNamespace($namespace); } else { $name = 'Unique' . $name; } } } // also loop new found use statements if (isset($this->useStatements[$name])) { // if the name is already used while (isset($this->useStatements[$name])) { if (($tmp = $this->extractClassNameOrAlias($namespace)) !== null) { $name = ucfirst($tmp) . $name; $namespace = $this->removeLastNameFromNamespace($namespace); } else { $name = 'Unique' . $name; } } } return $name; } /** * Extracts the last part of a namespace string, which is typically the class name. * * @param string $namespace The namespace string to extract from. * * @return string|null The extracted class name. * @since 3.2.0 */ protected function extractLastNameFromNamespace(string $namespace): ?string { $parts = explode('\\', $namespace); $result = end($parts); // Remove '\\' from the beginning and end of the resulting string $result = trim($result, '\\'); // If the resulting string is empty, return null return empty($result) ? null : $result; } /** * Removes the last name from the namespace. * * @param string $namespace The namespace * * @return string The namespace shortened * @since 3.2.0 */ protected function removeLastNameFromNamespace(string $namespace): string { // Remove '\\' from the beginning and end of the resulting string $namespace = trim($namespace, '\\'); $parts = explode('\\', $namespace); // Remove the last part (the class name) array_pop($parts); // Reassemble the namespace without the class name return implode('\\', $parts); } /** * Determines whether a trait statement should be added. * * @param object $power The power object. * * @return bool True if a trait statement should be added, false otherwise. * @since 3.2.0 */ protected function shouldAddTraitStatement(object $power): bool { return $power->type === 'trait'; } /** * Handles specific logic for traits, such as checking if the trait is already used. * * @param string $name The current name. * @param object $power The power object containing type, namespace, and class name. * @param array|null $traits The traits used in the code. * * @return void * @since 3.2.0 */ protected function handleTraitLogic(string $name, object $power, ?array $traits): void { if ($this->shouldAddTraitStatement($power) && $traits !== null) { foreach ($traits as $trait) { if ($trait === $name) { return; } } } // add the trait $this->traits[$name] = 'use ' . $name . ';'; } /** * Adds a use statement to the class if it's not already present. * * @param string $name The name to use. * @param string $className The class name of the power object. * @param string $namespaceStatement The search statement to search for (without the trailing semicolon, or use prefix). * * @since 3.2.0 */ protected function addUseStatement(string &$name, string $className, string $namespaceStatement): void { if ($name !== $className) { $statement = 'use ' . $namespaceStatement . ' as ' . $name . ';'; } else { $statement = 'use ' . $namespaceStatement . ';'; } $this->useStatements[$name] = $statement; } /** * Insert a line before the class declaration in the given class code. * * @param string $code The class code * @param array|null $useStatements The existing use statements * * @return string The modified file content * @since 3.2.0 */ protected function addUseStatements(string $code, ?array $useStatements): string { if ($useStatements !== null) { // we add the use statements using existing use statements $key = end($useStatements); array_unshift($this->useStatements, $key); return $this->placeholder->update($code, [$key => implode(PHP_EOL, array_values($this->useStatements))]); } return $this->addLines($code, implode(PHP_EOL, array_values($this->useStatements))); } /** * Insert a line before the class declaration in the given class code. * * @param string $code The class code * @param string $lines The new lines to insert * * @return string The modified file content * @since 3.2.0 */ protected function addLines(string $code, string $lines): string { // Pattern to match class, final class, abstract class, interface, and trait $pattern = '/(?:class|final class|abstract class|interface|trait)\s+[a-zA-Z0-9_]+\s*(?:extends\s+[a-zA-Z0-9_]+\s*)?(?:implements\s+[a-zA-Z0-9_]+(?:\s*,\s*[a-zA-Z0-9_]+)*)?\s*\{/s'; // Find the position of the class declaration preg_match($pattern, $code, $matches, PREG_OFFSET_CAPTURE); $class_declaration_pos = $matches[0][1] ?? null; if (null !== $class_declaration_pos) { // Find the position of the last newline character before the class declaration $last_newline_pos = strrpos($code, PHP_EOL, -(strlen($code) - $class_declaration_pos)); // Find the position of the comment block right before the class declaration $comment_pattern = '/\s*\*\/\s*$/m'; $insert_pos = null; if (preg_match($comment_pattern, $code, $comment_matches, PREG_OFFSET_CAPTURE, $last_newline_pos)) { $insert_pos = (int) $comment_matches[0][1] + strlen($comment_matches[0][0]); } else { // Find the last empty line before the class declaration $empty_line_pattern = '/(^|\r\n|\r|\n)[\s]*($|\r\n|\r|\n)/'; if (preg_match($empty_line_pattern, $code, $empty_line_matches, PREG_OFFSET_CAPTURE, $last_newline_pos)) { $insert_pos = (int) $empty_line_matches[0][1] + strlen($empty_line_matches[0][0]); } } // Insert the new line at the found position if (null !== $insert_pos) { return substr_replace($code, $lines . PHP_EOL, $insert_pos, 0); } } // last try targeting the defined line return $this->addLinesAfterDefinedLine($code, $lines); } /** * Inserts a new line after the defined('_JEXEC') line. * * @param string $code The class code * @param string $lines The new lines to insert * * @return string The modified file content * @since 3.2.0 */ protected function addLinesAfterDefinedLine(string $code, string $lines): string { // Patterns to match the defined('_JEXEC') and defined('JPATH_BASE') lines $patterns = [ "/defined\('_JEXEC'\)(.*?)\s*;/", "/\\defined\('_JEXEC'\)(.*?)\s*;/", "/defined\('JPATH_BASE'\)(.*?)\s*;/", "/\\defined\('JPATH_BASE'\)(.*?)\s*;/", ]; $insert_pos = null; // Iterate through the patterns and try to find a match foreach ($patterns as $pattern) { preg_match($pattern, $code, $matches, PREG_OFFSET_CAPTURE); $defined_line_pos = $matches[0][1] ?? null; if ($defined_line_pos !== null) { // Find the position of the newline character after the defined() line $next_lines_pos = strpos($code, PHP_EOL, (int) $defined_line_pos + strlen($matches[0][0])); // Insert the new line at the found position if ($next_lines_pos !== false) { $insert_pos = $next_lines_pos; break; } } } // Insert the new line at the found position if ($insert_pos !== null) { $code = substr_replace($code, PHP_EOL . $lines, $insert_pos, 0); } return $code; } }