From 2ca97958b7949758f326b680e219f92da9e8e8e3 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Sun, 19 Jul 2026 21:42:48 +0900 Subject: [PATCH 1/5] Add CI workflows for PHPUnit and coding standards Add continuous-integration.yml and coding-standards.yml calling the ray-di/.github reusable workflows (@v1), following the Ray.Aop setup. - PHPUnit: PHP 8.2, 8.3, 8.4 (old stable) and 8.5 (current stable) - Coding Standards: phpcs on PHP 8.3 --- .github/workflows/coding-standards.yml | 12 ++++++++++++ .github/workflows/continuous-integration.yml | 13 +++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 .github/workflows/coding-standards.yml create mode 100644 .github/workflows/continuous-integration.yml diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml new file mode 100644 index 0000000..52f86df --- /dev/null +++ b/.github/workflows/coding-standards.yml @@ -0,0 +1,12 @@ +name: Coding Standards + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + cs: + uses: ray-di/.github/.github/workflows/coding-standards.yml@v1 + with: + php_version: 8.3 \ No newline at end of file diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml new file mode 100644 index 0000000..9eb813a --- /dev/null +++ b/.github/workflows/continuous-integration.yml @@ -0,0 +1,13 @@ +name: Continuous Integration + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + ci: + uses: ray-di/.github/.github/workflows/continuous-integration.yml@v1 + with: + old_stable: '["8.2", "8.3", "8.4"]' + current_stable: 8.5 \ No newline at end of file From 47cda87b52346ee6a17b28b7fdd6e8f242cc2d07 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Sun, 19 Jul 2026 22:05:26 +0900 Subject: [PATCH 2/5] Align composer.json and phpcs.xml with Ray.Aop, run cs-fix composer.json: - Add description, config.sort-packages, scripts-descriptions - Drop friendsofphp/php-cs-fixer (v2 incompatible with PHP 8.5) - Bump squizlabs/php_codesniffer to ^4.0, add slevomat/coding-standard, doctrine/coding-standard, and dealerdirect/phpcodesniffer-composer-installer - Use bare commands (phpunit, phpcs, phpcbf); cs checks src tests, cs-fix runs phpcbf on src tests phpcs.xml: copied from Ray.Aop, edited for this project (name, schema path, project-specific excludes and patterns). cs-fix results: - phpcbf auto-fixed 181 violations across 16 files - Added :string return type to all __toString() methods - Removed empty if block in src/Arrow.php - Renamed test__invoke* to testInvoke* (PSR1 CamelCaps) Cleanup: remove orphaned .php_cs.dist, gitignore .phpcs-cache. Verified: composer cs passes (20 files), composer test passes (5 tests, 13 assertions). --- .gitignore | 1 + .php_cs.dist | 185 ------------------------------ composer.json | 43 ++++--- phpcs.xml | 85 +++++++++++--- src/Arrow.php | 31 ++--- src/ArrowInterface.php | 2 +- src/ClassId.php | 4 +- src/ClassNode.php | 41 ++++--- src/DependencyId.php | 10 +- src/Graph.php | 9 +- src/InstanceNode.php | 24 ++-- src/InterfaceNode.php | 37 +++--- src/MyDependency.php | 20 +--- src/NodeInterface.php | 2 +- src/ObjectGrapher.php | 106 ++++++++--------- src/Prop.php | 13 ++- src/ProviderNode.php | 18 ++- src/SnakeName.php | 4 +- src/ToClass.php | 18 +-- src/ToProvider.php | 10 +- tests/ObjectVisualGrapherTest.php | 27 +++-- tests/bin/package.php | 2 +- 22 files changed, 276 insertions(+), 416 deletions(-) delete mode 100644 .php_cs.dist diff --git a/.gitignore b/.gitignore index 88e2d70..ea59c2e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,6 @@ /composer.lock /phpunit.xml /.php_cs.cache +/.phpcs-cache .phpunit.result.cache /.phpunit.cache/ diff --git a/.php_cs.dist b/.php_cs.dist deleted file mode 100644 index e7a5cac..0000000 --- a/.php_cs.dist +++ /dev/null @@ -1,185 +0,0 @@ -exclude(['tests/Fake', 'tests/tmp', 'template']) - ->in(__DIR__); - -return \PhpCsFixer\Config::create() - ->setRiskyAllowed(true) - ->setRules([ - '@PSR2' => true, - '@PHP70Migration:risky' => true, - '@PHP71Migration:risky' => true, - 'align_multiline_comment' => true, - 'array_indentation' => true, - 'array_syntax' => ['syntax' => 'short'], - 'backtick_to_shell_exec' => true, - 'binary_operator_spaces' => true, // @Symfony - 'blank_line_after_opening_tag' => true, // @Symfony - 'blank_line_before_return' => true, - 'blank_line_before_statement' => ['statements' => ['break', 'continue', 'declare', 'return', 'throw']], // @Symfony - 'cast_spaces' => true, // @Symfony - 'class_attributes_separation' => ['elements' => ['const', 'method']], // @Symfony -// 'class_keyword_remove' => true, - 'combine_consecutive_issets' => true, - 'combine_consecutive_unsets' => true, -// 'comment_to_phpdoc' => true, - 'compact_nullable_typehint' => true, - 'concat_space' => ['spacing' => 'one'], // @Symfony -// 'date_time_immutable' => true, - 'declare_equal_normalize' => true, // @Symfony - 'dir_constant' => true, // @Symfony:risky - 'ereg_to_preg' => true, // @Symfony:risky - 'error_suppression' => true, // @Symfony:risky - 'escape_implicit_backslashes' => true, - 'explicit_indirect_variable' => true, - 'explicit_string_variable' => true, -// 'final_internal_class' => true, - 'fully_qualified_strict_types' => true, - 'function_to_constant' => true, // @Symfony:risky - 'function_typehint_space' => true, // @Symfony - 'general_phpdoc_annotation_remove' => ['author', 'category', 'package', 'copyright', 'version'], - 'header_comment' => ['header' => $header, 'comment_type' => 'comment'], - 'heredoc_to_nowdoc' => true, - 'include' => true, // @Symfony -// 'increment_style' => 'pre', // @Symfony - 'indentation_type' => true, - 'is_null' => ['use_yoda_style' => false], // @Symfony:risky - 'linebreak_after_opening_tag' => true, -// 'list_syntax' => true, -// 'logical_operators' => true, - 'lowercase_cast' => true, // @Symfony - 'lowercase_static_reference' => true, // @Symfony - 'magic_constant_casing' => true, -// 'mb_str_functions' => true, - 'method_chaining_indentation' => true, - 'method_separation' => true, - 'modernize_types_casting' => true, // @Symfony:risky - 'multiline_comment_opening_closing' => true, - 'multiline_whitespace_before_semicolons' => true, -// 'native_constant_invocation' => true, - 'native_function_casing' => true, // @Symfony -// 'native_function_invocation' => true, -// 'new_with_braces' => true, // @Symfony - 'no_alias_functions' => true, // @Symfony:risky - 'no_alternative_syntax' => true, - 'no_binary_string' => true, - 'no_blank_lines_after_class_opening' => true, // @Symfony - 'no_blank_lines_after_phpdoc' => true, // @Symfony -// 'no_blank_lines_before_namespace' => true, - 'no_empty_comment' => true, // @Symfony - 'no_empty_phpdoc' => true, // @Symfony - 'no_empty_statement' => true, // @Symfony - 'no_extra_blank_lines' => true, - 'no_homoglyph_names' => true, // @Symfony:risky - 'no_leading_import_slash' => true, // @Symfony - 'no_leading_namespace_whitespace' => true, // @Symfony - 'no_mixed_echo_print' => true, // @Symfony - 'no_multiline_whitespace_around_double_arrow' => true, // @Symfony - 'no_null_property_initialization' => true, - 'no_php4_constructor' => true, - 'no_short_bool_cast' => true, // @Symfony - 'no_short_echo_tag' => false, - 'no_singleline_whitespace_before_semicolons' => true, // @Symfony - 'no_spaces_around_offset' => true, // @Symfony - 'no_superfluous_elseif' => true, - 'no_superfluous_phpdoc_tags' => true, - 'no_trailing_comma_in_list_call' => true, // @Symfony - 'no_trailing_comma_in_singleline_array' => true, // @Symfony - 'no_trailing_whitespace' => true, - 'no_trailing_whitespace_in_comment' => true, - 'no_unneeded_control_parentheses' => true, // @Symfony - 'no_unneeded_curly_braces' => true, // @Symfony - 'no_unneeded_final_method' => true, // @Symfony - 'no_unreachable_default_argument_value' => true, - 'no_unset_on_property' => true, - 'no_unused_imports' => true, - 'no_useless_else' => true, - 'no_useless_return' => true, - 'no_whitespace_before_comma_in_array' => true, // @Symfony - 'no_whitespace_in_blank_line' => true, // @Symfony - 'non_printable_character' => true, // @Symfony - 'normalize_index_brace' => true, // @Symfony - 'not_operator_with_space' => false, - 'not_operator_with_successor_space' => true, - 'object_operator_without_whitespace' => true, // @Symfony - 'ordered_class_elements' => true, - 'ordered_imports' => true, - 'php_unit_construct' => true, // @Symfony:risky - 'php_unit_dedicate_assert' => true, - 'php_unit_expectation' => true, - 'php_unit_fqcn_annotation' => true, // @Symfony -// 'php_unit_internal_class' => true, // @Symfony] -// 'php_unit_mock' => true, - 'php_unit_namespaced' => true, - 'php_unit_no_expectation_annotation' => true, - 'php_unit_ordered_covers' => true, - 'php_unit_set_up_tear_down_visibility' => true, - 'php_unit_strict' => true, -// 'php_unit_test_annotation' => true, -// 'php_unit_test_case_static_method_calls' => true, -// 'php_unit_test_class_requires_covers' => true, -// 'phpdoc_add_missing_param_annotation' => true, - 'phpdoc_align' => true, // @Symfony] - 'phpdoc_annotation_without_dot' => true, // @Symfony] - 'phpdoc_indent' => true, // @Symfony] - 'phpdoc_inline_tag' => true, // @Symfony] - 'phpdoc_no_access' => true, // @Symfony] - 'phpdoc_no_alias_tag' => true, // @Symfony - 'phpdoc_no_empty_return' => true, // @Symfony - 'phpdoc_no_package' => true, // @Symfony -// 'phpdoc_no_useless_inheritdoc' => true, // @Symfony - 'phpdoc_order' => true, - 'phpdoc_return_self_reference' => true, // @Symfony - 'phpdoc_scalar' => true, // @Symfony - 'phpdoc_separation' => true, // @Symfony - 'phpdoc_single_line_var_spacing' => true, // @Symfony -// 'phpdoc_summary' => true, // @Symfony - 'phpdoc_to_comment' => true, // @Symfony - 'phpdoc_trim' => true, // @Symfony - 'phpdoc_trim_consecutive_blank_line_separation' => true, - 'phpdoc_types' => true, // @Symfony - 'phpdoc_types_order' => true, // @Symfony - 'phpdoc_var_without_name' => true, // @Symfony - 'protected_to_private' => true, - 'psr0' => true, - 'psr4' => true, // @Symfony:risky - 'return_assignment' => true, - 'return_type_declaration' => ['space_before' => 'one'], - 'self_accessor' => true, // @Symfony:risky - 'semicolon_after_instruction' => true, // @Symfony - 'set_type_to_cast' => true, // @Symfony:risky - 'short_scalar_cast' => true, // @Symfony:risky - 'simplified_null_return' => true, - 'single_blank_line_before_namespace' => true, // @Symfony -// 'single_line_comment_style' => true, // @Symfony - 'single_line_after_imports' => true, - 'single_quote' => true, // @Symfony - 'space_after_semicolon' => true, // @Symfony - 'standardize_increment' => true, // @Symfony - 'standardize_not_equals' => true, // @Symfony -// 'static_lambda' => true, -// 'strict_comparison' => true, - 'strict_param' => true, -// 'string_line_ending' => true, - 'switch_case_semicolon_to_colon' => true, - 'switch_case_space' => true, - 'ternary_operator_spaces' => true, -// 'trailing_comma_in_multiline_array' => true, // @Symfony - 'trim_array_spaces' => true, // @Symfony - 'unary_operator_spaces' => true, // @Symfony - 'visibility_required' => true, -// 'void_return' => true, // @PHP71Migration:risky - 'whitespace_after_comma_in_array' => true, // @Symfony -// 'yoda_style' => true // @Symfony - ]) - ->setFinder($finder); diff --git a/composer.json b/composer.json index 22394c0..20acee9 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "ray/object-visual-grapher", - "description": "", + "description": "A visual graph generator for Ray.Di object dependency graphs", "license": "MIT", "authors": [ { @@ -12,14 +12,22 @@ "php": "^8.2" }, "require-dev": { - "phpunit/phpunit": "^10.5", - "squizlabs/php_codesniffer": "^3.2", - "friendsofphp/php-cs-fixer": "^2.11", - "phpmd/phpmd": "^2.6", - "ray/di": "^2.10|2.x-dev", "bear/package": "^1.9.6", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", "doctrine/annotations": "^1.8", - "phpstan/phpstan": "^0.12.5" + "doctrine/coding-standard": "^14.0", + "phpmd/phpmd": "^2.6", + "phpstan/phpstan": "^0.12.5", + "phpunit/phpunit": "^10.5", + "ray/di": "^2.10|2.x-dev", + "slevomat/coding-standard": "^8.15", + "squizlabs/php_codesniffer": "^4.0" + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } }, "autoload": { "psr-4": { @@ -39,23 +47,28 @@ "php ./tests/bin/package.php && dot -T png ./tests/bin/package.dot -o ./tests/bin/package.png && open ./tests/bin/package.png" ], "test": [ - "vendor/bin/phpunit" + "phpunit" ], "tests": [ "@cs", "@test", - "vendor/bin/phpstan analyse -l max src -c phpstan.neon --no-progress" + "phpstan analyse -l max src -c phpstan.neon --no-progress" ], "coverage": [ - "php -dzend_extension=xdebug.so ./vendor/bin/phpunit --coverage-text --coverage-html=build/coverage" + "php -dzend_extension=xdebug.so -dxdebug.mode=coverage ./vendor/bin/phpunit --coverage-text --coverage-html=build/coverage" ], "cs": [ - "vendor/bin/php-cs-fixer fix -v --dry-run", - "vendor/bin/phpcs --standard=phpcs.xml src;" + "phpcs --standard=./phpcs.xml src tests" ], "cs-fix": [ - "vendor/bin/php-cs-fixer fix -v", - "vendor/bin/phpcbf src tests" + "phpcbf src tests" ] + }, + "scripts-descriptions": { + "test": "Run unit tests", + "tests": "Run coding standards, tests, and static analysis", + "coverage": "Generate test coverage report with Xdebug", + "cs": "Check coding standards", + "cs-fix": "Fix coding standards violations" } -} +} \ No newline at end of file diff --git a/phpcs.xml b/phpcs.xml index 460d322..793d4fb 100755 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,18 +1,71 @@ - - - - - - - + + + + + + + + + + + + + + + + + src + tests + */tests/Fake/* + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + + + + + + + + - + + + + + + + + \ No newline at end of file diff --git a/src/Arrow.php b/src/Arrow.php index 19042ca..353f745 100644 --- a/src/Arrow.php +++ b/src/Arrow.php @@ -7,31 +7,23 @@ use Ray\Aop\ReflectionClass; use Ray\Di\ProviderInterface; +use function class_exists; + final class Arrow implements ArrowInterface { - /** - * @var array - */ + /** @var array */ public static $history; - /** - * @var string - */ + /** @var string */ public $class; - /** - * @var string - */ + /** @var string */ private $interface; - /** - * @var string - */ + /** @var string */ private $arrowHead; - /** - * @var bool - */ + /** @var bool */ private $isInvalid; public function __construct(string $fromId, string $toId, string $toClass) @@ -41,15 +33,14 @@ public function __construct(string $fromId, string $toId, string $toClass) $isProvider = class_exists($toClass) && (new ReflectionClass($toClass))->implementsInterface(ProviderInterface::class); $this->arrowHead = $isProvider ? 'arrowhead=onormalonormal' : 'arrowhead=onormal'; $index = $fromId . $toId . $toClass; - if (isset(self::$history[$index])) { - // log duplicated (mostly @depracated) bindings ( - // error_log(sprintf('from:%s to:%s class:%s index:%s ', $fromId, $toClass, $toClass, $index)); - } + // log duplicated (mostly @depracated) bindings ( + // error_log(sprintf('from:%s to:%s class:%s index:%s ', $fromId, $toClass, $toClass, $index)); + $this->isInvalid = isset(self::$history[$index]); self::$history[$index] = true; } - public function __toString() + public function __toString(): string { return $this->isInvalid ? '' : <<interface} -> {$this->class} [style=solid, arrowtail=none, {$this->arrowHead}] diff --git a/src/ArrowInterface.php b/src/ArrowInterface.php index beb93ab..cdcc9b4 100644 --- a/src/ArrowInterface.php +++ b/src/ArrowInterface.php @@ -6,5 +6,5 @@ interface ArrowInterface { - public function __toString(); + public function __toString(): string; } diff --git a/src/ClassId.php b/src/ClassId.php index 72fed29..292be16 100644 --- a/src/ClassId.php +++ b/src/ClassId.php @@ -6,8 +6,8 @@ final class ClassId { - public function __invoke(string $class) : string + public function __invoke(string $class): string { - return 'class_' . (new SnakeName)($class); + return 'class_' . (new SnakeName())($class); } } diff --git a/src/ClassNode.php b/src/ClassNode.php index 261089a..9db8cb9 100644 --- a/src/ClassNode.php +++ b/src/ClassNode.php @@ -4,36 +4,34 @@ namespace Ray\ObjectGrapher; +use ReflectionClass; +use ReflectionException; + +use function assert; +use function class_exists; +use function in_array; +use function str_replace; + +use const PHP_EOL; + final class ClassNode implements NodeInterface { - /** - * @var array - */ + /** @var array */ public static $ids = []; - /** - * @var string - */ + /** @var string */ private $id; - /** - * @var string - */ + /** @var string */ private $namespace; - /** - * @var string - */ + /** @var string */ private $class; - /** - * @var string - */ + /** @var string */ private $settersTable = ''; - /** - * @var bool - */ + /** @var bool */ private $invalid = false; /** @@ -41,7 +39,7 @@ final class ClassNode implements NodeInterface * @param string $class class * @param array $setters * - * @throws \ReflectionException + * @throws ReflectionException */ public function __construct(string $id, string $class, array $setters) { @@ -50,10 +48,11 @@ public function __construct(string $id, string $class, array $setters) return; } + self::$ids[] = $id; $this->id = $id; assert(class_exists($class)); - $ref = new \ReflectionClass($class); + $ref = new ReflectionClass($class); $this->namespace = str_replace('\\', '\\\\', $ref->getNamespaceName()); $this->class = $ref->getShortName(); foreach ($setters as $setterMethod => $port) { @@ -61,7 +60,7 @@ public function __construct(string $id, string $class, array $setters) } } - public function __toString() + public function __toString(): string { $html = $this->invalid ? '' : /* @lang html */ <<< EOT {$this->id} [style=solid, margin=0.02, label= diff --git a/src/DependencyId.php b/src/DependencyId.php index 5877054..e1fb5a1 100644 --- a/src/DependencyId.php +++ b/src/DependencyId.php @@ -4,14 +4,18 @@ namespace Ray\ObjectGrapher; +use function class_exists; +use function sprintf; + final class DependencyId { - public function __invoke(string $interace, string $name) : string + public function __invoke(string $interace, string $name): string { if (class_exists($interace)) { - return (new ClassId)($interace); + return (new ClassId())($interace); } - $snakeName = new SnakeName; + + $snakeName = new SnakeName(); return sprintf('dependency_%s_%s', ($snakeName)($interace), ($snakeName)($name)); } diff --git a/src/Graph.php b/src/Graph.php index 3de5dc4..d30579c 100644 --- a/src/Graph.php +++ b/src/Graph.php @@ -4,6 +4,8 @@ namespace Ray\ObjectGrapher; +use const PHP_EOL; + final class Graph { /** @@ -20,12 +22,13 @@ final class Graph */ private $arrows; - public function __toString() + public function __toString(): string { $string = ''; foreach ($this->nodes as $node) { $string .= (string) $node . PHP_EOL; } + $string .= PHP_EOL; foreach ($this->arrows as $arrow) { $string .= (string) $arrow . PHP_EOL; @@ -34,12 +37,12 @@ public function __toString() return $string; } - public function addNode(NodeInterface $node) : void + public function addNode(NodeInterface $node): void { $this->nodes[] = $node; } - public function addArrow(ArrowInterface $arrow) : void + public function addArrow(ArrowInterface $arrow): void { $this->arrows[] = $arrow; } diff --git a/src/InstanceNode.php b/src/InstanceNode.php index f50202e..00baf98 100644 --- a/src/InstanceNode.php +++ b/src/InstanceNode.php @@ -6,26 +6,24 @@ use Ray\Di\Instance; +use function addslashes; +use function gettype; +use function str_replace; + +use const PHP_EOL; + final class InstanceNode implements NodeInterface { - /** - * @var string - */ + /** @var string */ private $id; - /** - * @var string - */ + /** @var string */ private $interface; - /** - * @var string - */ + /** @var string */ private $type; - /** - * @var string - */ + /** @var string */ private $named; public function __construct(string $id, string $interface, string $named, Instance $instance) @@ -36,7 +34,7 @@ public function __construct(string $id, string $interface, string $named, Instan $this->type = gettype($instance->value); } - public function __toString() + public function __toString(): string { $html = /* @lang html */ <<< EOT {$this->id} [style=dashed, margin=0.02, label= diff --git a/src/InterfaceNode.php b/src/InterfaceNode.php index c9747c9..f35a52b 100644 --- a/src/InterfaceNode.php +++ b/src/InterfaceNode.php @@ -4,31 +4,30 @@ namespace Ray\ObjectGrapher; +use ReflectionClass; + +use function assert; +use function class_exists; +use function interface_exists; +use function str_replace; + +use const PHP_EOL; + final class InterfaceNode implements NodeInterface { - /** - * @var string - */ + /** @var string */ private $id; - /** - * @var string - */ + /** @var string */ private $namespace; - /** - * @var string - */ + /** @var string */ private $interface; - /** - * @var string - */ + /** @var string */ private $named; - /** - * @var string - */ + /** @var string */ private $bgColor; public function __construct(string $id, string $interface, string $named) @@ -39,7 +38,7 @@ public function __construct(string $id, string $interface, string $named) $this->bgColor = $interface ? 'ffffff' : 'aaaaaa'; } - public function __toString() + public function __toString(): string { $html = /* @lang html */ <<< EOT {$this->id} [style=dashed, margin=0.02, label= @@ -56,15 +55,15 @@ public function __toString() return str_replace(PHP_EOL, '', $html); } - public function setInstance() : void + public function setInstance(): void { $this->interface = 'instance'; } - private function setNamespace(string $interface) : void + private function setNamespace(string $interface): void { assert(interface_exists($interface) || class_exists($interface)); - $ref = new \ReflectionClass($interface); + $ref = new ReflectionClass($interface); $this->namespace = str_replace('\\', '\\\\', $ref->getNamespaceName()); $this->interface = $ref->getShortName(); } diff --git a/src/MyDependency.php b/src/MyDependency.php index 4a33430..18d9734 100644 --- a/src/MyDependency.php +++ b/src/MyDependency.php @@ -10,32 +10,24 @@ final class MyDependency { - /** - * @var string - */ + /** @var string */ public $class; - /** - * @var string - */ + /** @var string */ public $classId; - /** - * @var null|Arguments - */ + /** @var Arguments|null */ public $arguments; - /** - * @var SetterMethod - */ + /** @var SetterMethod */ public $setterMethods; public function __construct(Dependency $dependency) { - $prop = new Prop; + $prop = new Prop(); $newInstance = $prop($dependency, 'newInstance'); $this->class = $prop($newInstance, 'class'); - $this->classId = (new ClassId)($this->class); + $this->classId = (new ClassId())($this->class); $this->arguments = $prop($newInstance, 'arguments'); $this->setterMethods = $prop($newInstance, 'setterMethods'); } diff --git a/src/NodeInterface.php b/src/NodeInterface.php index d4272ab..7c4e0ea 100644 --- a/src/NodeInterface.php +++ b/src/NodeInterface.php @@ -6,5 +6,5 @@ interface NodeInterface { - public function __toString(); + public function __toString(): string; } diff --git a/src/ObjectGrapher.php b/src/ObjectGrapher.php index 64c90e3..014e917 100644 --- a/src/ObjectGrapher.php +++ b/src/ObjectGrapher.php @@ -12,58 +12,51 @@ use Ray\Di\DependencyInterface; use Ray\Di\DependencyProvider; use Ray\Di\Instance; +use ReflectionClass; +use ReflectionException; + +use function assert; +use function class_exists; +use function explode; +use function interface_exists; +use function sprintf; final class ObjectGrapher { - /** - * @var Prop - */ + /** @var Prop */ private $prop; - /** - * @var Container - */ + /** @var Container */ private $container; - /** - * @var Container - */ + /** @var Container */ private $analysisContainer; - /** - * @var array - */ + /** @var array */ private $dependencies = []; - /** - * @var Graph - */ + + /** @var Graph */ private $graph; - /** - * @var DependencyId - */ + /** @var DependencyId */ private $dependencyId; - /** - * @var ClassId - */ + /** @var ClassId */ private $classId; - /** - * @var SnakeName - */ + /** @var SnakeName */ private $snakeName; public function __construct() { - $this->prop = new Prop; - $this->graph = new Graph; - $this->dependencyId = new DependencyId; - $this->classId = new ClassId; - $this->snakeName = new SnakeName; + $this->prop = new Prop(); + $this->graph = new Graph(); + $this->dependencyId = new DependencyId(); + $this->classId = new ClassId(); + $this->snakeName = new SnakeName(); } - public function __invoke(AbstractModule $module) : string + public function __invoke(AbstractModule $module): string { $this->init(); $this->container = $module->getContainer(); @@ -75,42 +68,48 @@ public function __invoke(AbstractModule $module) : string return $this->toString(); } - private function init() : void + private function init(): void { Arrow::$history = ToClass::$index = ClassNode::$ids = []; - $this->graph = new Graph; - $this->analysisContainer = new Container; + $this->graph = new Graph(); + $this->analysisContainer = new Container(); $this->dependencies = []; } - private function setGraph(string $type, string $name, DependencyInterface $dependency) : void + private function setGraph(string $type, string $name, DependencyInterface $dependency): void { $isTargetBinding = ! interface_exists($type); $dependencyId = ($this->dependencyId)($type, $name); if (! $isTargetBinding) { $this->graph->addNode(new InterfaceNode($dependencyId, $type, $name)); } + if ($dependency instanceof Instance) { $this->graph->addNode(new InstanceNode($dependencyId, $type, $name, $dependency)); } + if ($dependency instanceof Dependency) { $this->dependencyNode($dependencyId, new MyDependency($dependency), $isTargetBinding); } - if ($dependency instanceof DependencyProvider) { - $this->providerNode($dependencyId, $dependency); + + if (! ($dependency instanceof DependencyProvider)) { + return; } + + $this->providerNode($dependencyId, $dependency); } - private function dependencyNode(string $interfaceId, MyDependency $dependency, bool $isTargetBinding) : void + private function dependencyNode(string $interfaceId, MyDependency $dependency, bool $isTargetBinding): void { if (! $isTargetBinding) { $this->graph->addArrow(new ToClass($interfaceId, $dependency->classId)); } + $setters = $this->lineDependency($dependency); $this->graph->addNode(new ClassNode($dependency->classId, $dependency->class, $setters)); } - private function providerNode(string $interfaceId, DependencyProvider $dependency) : void + private function providerNode(string $interfaceId, DependencyProvider $dependency): void { $dependency = new MyDependency(($this->prop)($dependency, 'dependency')); $setters = $this->lineDependency($dependency); @@ -118,17 +117,17 @@ private function providerNode(string $interfaceId, DependencyProvider $dependenc if ($interfaceId) { $this->graph->addArrow(new ToProvider($interfaceId, $dependency->classId)); } + $this->graph->addNode(new ClassNode($dependency->classId, $dependency->class, $setters)); } - /** - * @return array setter symbol - */ - private function lineDependency(MyDependency $dependency) : array + /** @return array setter symbol */ + private function lineDependency(MyDependency $dependency): array { if (! $dependency->arguments) { return []; } + // constructor injection $arguments = ($this->prop)($dependency->arguments, 'arguments'); $port = sprintf('p_%s_construct', ($this->snakeName)($dependency->class)); @@ -148,9 +147,9 @@ private function lineDependency(MyDependency $dependency) : array return $setters; } - private function setterArrow(string $classPort, string $dependencyIndex) : void + private function setterArrow(string $classPort, string $dependencyIndex): void { - [$type, $name] = \explode('-', $dependencyIndex); + [$type, $name] = explode('-', $dependencyIndex); $dependencyId = ($this->dependencyId)($type, $name); $this->graph->addArrow(new Arrow($classPort, $dependencyId, $type)); if (class_exists($type)) { @@ -158,19 +157,21 @@ private function setterArrow(string $classPort, string $dependencyIndex) : void return; } + $this->graph->addNode(new InterfaceNode($dependencyId, $type, $name)); } - private function addClassNode(string $dependencyIndex) : void + private function addClassNode(string $dependencyIndex): void { - [$type, $name] = \explode('-', $dependencyIndex); + [$type, $name] = explode('-', $dependencyIndex); assert(class_exists($type)); - $isAbstract = (new \ReflectionClass($type))->isAbstract(); + $isAbstract = (new ReflectionClass($type))->isAbstract(); if ($isAbstract) { $this->graph->addNode(new InterfaceNode(($this->classId)($type), $type, $name)); return; } + $container = $this->container->getContainer(); if (isset($container[$dependencyIndex]) || isset($this->dependencies[$dependencyIndex])) { return; @@ -184,7 +185,7 @@ private function addClassNode(string $dependencyIndex) : void * @param string $classId class ID * @param string $port port ID */ - private function drawInjectionGraph(array $arguments, string $classId, string $port) : void + private function drawInjectionGraph(array $arguments, string $classId, string $port): void { foreach ($arguments as $argument) { assert($argument instanceof Argument); @@ -194,16 +195,15 @@ private function drawInjectionGraph(array $arguments, string $classId, string $p } } - /** - * @throws \ReflectionException - */ - private function bindOnTheFly(string $dependencyIndex, string $type, string $name) : void + /** @throws ReflectionException */ + private function bindOnTheFly(string $dependencyIndex, string $type, string $name): void { /** @var class-string $type */ $bind = new Bind($this->analysisContainer, $type); if ($name !== '') { $bind->annotatedWith($name); } + $bind->to($type); $dependency = $this->analysisContainer->getContainer()[$dependencyIndex]; assert($dependency instanceof Dependency); @@ -212,7 +212,7 @@ private function bindOnTheFly(string $dependencyIndex, string $type, string $nam $this->graph->addNode(new ClassNode(($this->classId)($type), $type, $setters)); } - private function toString() : string + private function toString(): string { return <<getProperty($prop); - } catch (\ReflectionException $e) { + $property = (new ReflectionClass($object::class))->getProperty($prop); + } catch (ReflectionException) { return ''; } diff --git a/src/ProviderNode.php b/src/ProviderNode.php index 863f18a..d021ef6 100644 --- a/src/ProviderNode.php +++ b/src/ProviderNode.php @@ -4,21 +4,19 @@ namespace Ray\ObjectGrapher; +use function str_replace; + +use const PHP_EOL; + final class ProviderNode implements NodeInterface { - /** - * @var string - */ + /** @var string */ private $id; - /** - * @var string - */ + /** @var string */ private $provider; - /** - * @var string - */ + /** @var string */ private $settersTable = ''; /** @@ -35,7 +33,7 @@ public function __construct(string $id, string $provider, array $setters) } } - public function __toString() + public function __toString(): string { $html = /* @lang html */ <<< EOT {$this->id} [style=solid, margin=0.02, label=< diff --git a/src/SnakeName.php b/src/SnakeName.php index 2d7aaa2..0900f62 100644 --- a/src/SnakeName.php +++ b/src/SnakeName.php @@ -4,9 +4,11 @@ namespace Ray\ObjectGrapher; +use function str_replace; + final class SnakeName { - public function __invoke(string $class) : string + public function __invoke(string $class): string { return str_replace('\\', '_', $class); } diff --git a/src/ToClass.php b/src/ToClass.php index d20b680..1f0b316 100644 --- a/src/ToClass.php +++ b/src/ToClass.php @@ -6,24 +6,16 @@ final class ToClass implements ArrowInterface { - /** - * @var array - */ + /** @var array */ public static $index; - /** - * @var string - */ + /** @var string */ public $class; - /** - * @var string - */ + /** @var string */ private $interface; - /** - * @var bool - */ + /** @var bool */ private $noArrow; public function __construct(string $interfaceId, string $classId) @@ -35,7 +27,7 @@ public function __construct(string $interfaceId, string $classId) self::$index[$indexId] = true; } - public function __toString() + public function __toString(): string { return $this->noArrow ? '' : <<interface} -> {$this->class} [style=dashed, arrowtail=none, arrowhead=onormal] diff --git a/src/ToProvider.php b/src/ToProvider.php index d1f21a3..ad5b1d9 100644 --- a/src/ToProvider.php +++ b/src/ToProvider.php @@ -6,14 +6,10 @@ final class ToProvider implements ArrowInterface { - /** - * @var string - */ + /** @var string */ public $class; - /** - * @var string - */ + /** @var string */ private $interface; public function __construct(string $interface, string $class) @@ -22,7 +18,7 @@ public function __construct(string $interface, string $class) $this->interface = $interface; } - public function __toString() + public function __toString(): string { return <<interface} -> {$this->class} [style=dashed, arrowtail=none, arrowhead=onormalonormal] diff --git a/tests/ObjectVisualGrapherTest.php b/tests/ObjectVisualGrapherTest.php index fb670a9..1ca1cb1 100644 --- a/tests/ObjectVisualGrapherTest.php +++ b/tests/ObjectVisualGrapherTest.php @@ -4,32 +4,31 @@ namespace Ray\ObjectGrapher; -use function array_keys; use BEAR\Package\PackageModule; use BEAR\Resource\Module\ResourceModule; use PHPUnit\Framework\TestCase; use Ray\Aop\Matcher; use Ray\Aop\Pointcut; +use function array_keys; + class ObjectVisualGrapherTest extends TestCase { - /** - * @var ObjectGrapher - */ + /** @var ObjectGrapher */ protected $objectGrapher; - protected function setUp() : void + protected function setUp(): void { - $this->objectGrapher = new ObjectGrapher; + $this->objectGrapher = new ObjectGrapher(); } - public function testIsInstanceOfObjectGrapher() : void + public function testIsInstanceOfObjectGrapher(): void { $actual = $this->objectGrapher; $this->assertInstanceOf(ObjectGrapher::class, $actual); } - public function test__invoke() : void + public function testInvoke(): void { $dot = ($this->objectGrapher)(new FakeModule()); $this->assertStringContainsString('dependency_Ray_ObjectGrapher_LoggerInterface_ -> class_Ray_ObjectGrapher_DatabaseLogger', $dot); @@ -38,7 +37,7 @@ public function test__invoke() : void $this->assertStringContainsString('dependency_Ray_ObjectGrapher_PdoInterface_ -> class_Ray_ObjectGrapher_PdoProvider [style=dashed, arrowtail=none, arrowhead=onormalonormal]', $dot); } - public function test__invokeBearResource() : void + public function testInvokeBearResource(): void { $dot = ($this->objectGrapher)(new ResourceModule('a')); $this->assertStringContainsString('dependency__BEAR_Resource_Annotation_AppName [style', $dot); @@ -46,17 +45,17 @@ public function test__invokeBearResource() : void $this->assertStringContainsString('dependency_BEAR_Resource_SchemeCollectionInterface_ -> class_BEAR_Resource_Module_SchemeCollectionProvider [style=dashed, arrowtail=none, arrowhead=onormalonormal]', $dot); } - public function test__invokeBearPackage() : void + public function testInvokeBearPackage(): void { - $dot = ($this->objectGrapher)(new PackageModule); + $dot = ($this->objectGrapher)(new PackageModule()); $this->assertStringContainsString('_BEAR_Sunday_Provide_Transfer_ConditionalResponseInterface_ -> class_BEAR_Sunday_Provide_Transfer_ConditionalResponse [style=dashed, arrowtail=none, arrowhead=onormal]', $dot); } - public function testConcreteDependencyAnalysisDoesNotMutateModule() : void + public function testConcreteDependencyAnalysisDoesNotMutateModule(): void { - $module = new ConcreteModule; + $module = new ConcreteModule(); $container = $module->getContainer(); - $container->addPointcut(new Pointcut((new Matcher)->any(), (new Matcher)->any(), [DatabaseLogger::class])); + $container->addPointcut(new Pointcut((new Matcher())->any(), (new Matcher())->any(), [DatabaseLogger::class])); $keys = array_keys($container->getContainer()); $pointcuts = $container->getPointcuts(); diff --git a/tests/bin/package.php b/tests/bin/package.php index da340ed..89c0ac6 100644 --- a/tests/bin/package.php +++ b/tests/bin/package.php @@ -9,6 +9,6 @@ use BEAR\Package\PackageModule; use Ray\ObjectGrapher\ObjectGrapher; -$dot = (new ObjectGrapher)(new AppMetaModule(new Meta('Ray\ObjectGrapher'), new PackageModule)); +$dot = (new ObjectGrapher())(new AppMetaModule(new Meta('Ray\ObjectGrapher'), new PackageModule())); $file = __DIR__ . '/package.dot'; file_put_contents($file, $dot); From fe355671d8f7855c169ee2593e7d3c7242436aa5 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Sun, 19 Jul 2026 22:43:47 +0900 Subject: [PATCH 3/5] Upgrade PHPStan to 2.x and add static analysis CI - Upgrade phpstan/phpstan from ^0.12.5 to ^2.0; the 0.12 phar emitted PHP 8.5 deprecation noise from its own bundled Symfony console and hoa compiler and was effectively unusable on modern PHP. - Populate phpstan.neon (was empty): level 8, phpVersion 80200, paths: [src]. Level 8 chosen over max because Prop::__invoke reads Ray.Di internal objects via reflection and returns mixed; the mixed assignments flagged at max stem from untyped upstream Ray.Di properties, not fixable here without ignoring/casting. - Remove redundant assert( instanceof Argument) in ObjectGrapher::drawInjectionGraph(); the @param array PHPDoc already narrows the type, flagged by PHPStan 2.x as function.alreadyNarrowedType / instanceof.alwaysTrue. - composer tests: phpstan now reads level/paths from phpstan.neon. - Add .github/workflows/static-analysis.yml running PHPStan in CI (cs2pr-annotated), on PHP 8.3. Verified: composer cs (20/20), composer test (5 tests), phpstan (level 8) all pass. --- .github/workflows/static-analysis.yml | 40 +++++++++++++++++++++++++++ composer.json | 6 ++-- phpstan.neon | 5 ++++ src/ObjectGrapher.php | 1 - 4 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/static-analysis.yml diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 0000000..6e39da0 --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,40 @@ +name: Static Analysis + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + coverage: none + tools: cs2pr + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer install --no-interaction --no-progress --prefer-dist + + - name: Run PHPStan + run: ./vendor/bin/phpstan analyse -c phpstan.neon --no-progress --no-interaction --error-format=checkstyle | cs2pr \ No newline at end of file diff --git a/composer.json b/composer.json index 20acee9..1631b07 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "doctrine/annotations": "^1.8", "doctrine/coding-standard": "^14.0", "phpmd/phpmd": "^2.6", - "phpstan/phpstan": "^0.12.5", + "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^10.5", "ray/di": "^2.10|2.x-dev", "slevomat/coding-standard": "^8.15", @@ -52,7 +52,7 @@ "tests": [ "@cs", "@test", - "phpstan analyse -l max src -c phpstan.neon --no-progress" + "phpstan analyse -c phpstan.neon --no-progress" ], "coverage": [ "php -dzend_extension=xdebug.so -dxdebug.mode=coverage ./vendor/bin/phpunit --coverage-text --coverage-html=build/coverage" @@ -71,4 +71,4 @@ "cs": "Check coding standards", "cs-fix": "Fix coding standards violations" } -} \ No newline at end of file +} diff --git a/phpstan.neon b/phpstan.neon index e69de29..2de7ef5 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: 8 + phpVersion: 80200 + paths: + - src \ No newline at end of file diff --git a/src/ObjectGrapher.php b/src/ObjectGrapher.php index 014e917..de66b76 100644 --- a/src/ObjectGrapher.php +++ b/src/ObjectGrapher.php @@ -188,7 +188,6 @@ private function addClassNode(string $dependencyIndex): void private function drawInjectionGraph(array $arguments, string $classId, string $port): void { foreach ($arguments as $argument) { - assert($argument instanceof Argument); $dependencyIndex = ($this->prop)($argument, 'index'); $classPort = sprintf('%s:%s:e', $classId, $port); $this->setterArrow($classPort, $dependencyIndex); From 51ac3985aad36029c6b257aa0947c04c2146e2e1 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Mon, 20 Jul 2026 19:28:25 +0900 Subject: [PATCH 4/5] Fix prefer-lowest CI: pin bear/query-repository ^1.13, drop installer plugin CI failed on --prefer-lowest: bear/package ^1.9.6 resolves bear/query-repository 1.6.0, whose QueryRepositoryModule binds Doctrine\Common\Cache\Cache, but doctrine/cache is not installed -> Ray\Di\Exception\NotFound in testInvokeBearPackage. bear/package cannot be bumped to sidestep this: every bear/package >= 1.10.10 self-requires ray/object-visual-grapher ^1.0, which is circular for this repo's own dev install (root is dev-master). So pin bear/query-repository ^1.13 instead -- 1.13.0 dropped the Doctrine Cache binding and the doctrine/* requires. Tests pass at both --prefer-lowest and highest. Remove dealerdirect/phpcodesniffer-composer-installer from require-dev (still pulled transitively by doctrine/coding-standard and slevomat/coding-standard) and disable its plugin execution via allow-plugins=false; register the external standards via in phpcs.xml instead. Drop the unused doctrine/annotations ^1.8 direct dev dependency. Verified: composer cs (20/20), phpstan (level 8, no errors), phpunit (5 tests) pass at both highest and --prefer-lowest. --- composer.json | 5 ++--- phpcs.xml | 2 ++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 1631b07..e253c7d 100644 --- a/composer.json +++ b/composer.json @@ -13,8 +13,7 @@ }, "require-dev": { "bear/package": "^1.9.6", - "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "doctrine/annotations": "^1.8", + "bear/query-repository": "^1.13", "doctrine/coding-standard": "^14.0", "phpmd/phpmd": "^2.6", "phpstan/phpstan": "^2.0", @@ -26,7 +25,7 @@ "config": { "sort-packages": true, "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true + "dealerdirect/phpcodesniffer-composer-installer": false } }, "autoload": { diff --git a/phpcs.xml b/phpcs.xml index 793d4fb..19042e4 100755 --- a/phpcs.xml +++ b/phpcs.xml @@ -11,6 +11,8 @@ + + From c663807805ffd2d8e357fc2b89626a9a7b3adb76 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Mon, 27 Jul 2026 09:26:36 +0900 Subject: [PATCH 5/5] Harden static-analysis workflow: read-only token, no credential persistence Address CodeRabbit/zizmor artipacked warning on PR #5: set permissions: contents: read on the phpstan job and persist-credentials: false on actions/checkout. --- .github/workflows/static-analysis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 6e39da0..79ea4f6 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -9,11 +9,14 @@ jobs: phpstan: name: PHPStan runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 + persist-credentials: false - name: Setup PHP uses: shivammathur/setup-php@v2
{$this->provider}