custom-loader.php 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. <?php
  2. /**
  3. * Yoast SEO Plugin File.
  4. *
  5. * @package Yoast\YoastSEO\Dependency_Injection
  6. */
  7. namespace Yoast\WP\Free\Dependency_Injection;
  8. use Symfony\Component\Config\FileLocator;
  9. use Symfony\Component\DependencyInjection\ContainerBuilder;
  10. use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
  11. use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
  12. use Symfony\Component\Config\Resource\GlobResource;
  13. use Symfony\Component\DependencyInjection\ChildDefinition;
  14. use Symfony\Component\DependencyInjection\Definition;
  15. /**
  16. * This class is mostly a direct copy-paste of the symfony PhpFileLoader class.
  17. * It's been adapted to allow automatic discovery based on not just PSR-4 but also the Yoast standards.
  18. */
  19. class Custom_Loader extends PhpFileLoader {
  20. /**
  21. * Custom_Loader constructor.
  22. *
  23. * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container The ContainerBuilder to load classes for.
  24. */
  25. public function __construct( ContainerBuilder $container ) {
  26. parent::__construct( $container, new FileLocator( __DIR__ . '/../..' ) );
  27. }
  28. /**
  29. * Transforms a path to a class name using the class map.
  30. *
  31. * @param string $path The path of the class.
  32. *
  33. * @return bool|string The classname.
  34. */
  35. private function getClassFromClassMap( $path ) {
  36. static $class_map;
  37. if ( ! $class_map ) {
  38. $class_map = require __DIR__ . '/../../vendor/composer/autoload_classmap.php';
  39. }
  40. foreach ( $class_map as $class => $class_path ) {
  41. if ( $path === $class_path ) {
  42. return $class;
  43. }
  44. }
  45. return false;
  46. }
  47. /**
  48. * Registers a set of classes as services using PSR-4 for discovery.
  49. *
  50. * @param \Symfony\Component\DependencyInjection\Definition $prototype A definition to use as template.
  51. * @param string $namespace The namespace prefix of classes
  52. * in the scanned directory.
  53. * @param string $resource The directory to look for classes,
  54. * glob-patterns allowed.
  55. * @param string $exclude A globed path of files to exclude.
  56. *
  57. * @throws InvalidArgumentException If invalid arguments are supplied.
  58. *
  59. * @return void
  60. */
  61. public function registerClasses( Definition $prototype, $namespace, $resource, $exclude = null ) {
  62. if ( '\\' !== \substr( $namespace, -1 ) ) {
  63. throw new InvalidArgumentException( \sprintf( 'Namespace prefix must end with a "\\": %s.', $namespace ) );
  64. }
  65. if ( ! \preg_match( '/^(?:[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+\\\\)++$/', $namespace ) ) {
  66. throw new InvalidArgumentException( \sprintf( 'Namespace is not a valid PSR-4 prefix: %s.', $namespace ) );
  67. }
  68. $classes = $this->findClasses( $namespace, $resource, $exclude );
  69. // Prepare for deep cloning.
  70. $serialized_prototype = \serialize( $prototype );
  71. $interfaces = [];
  72. $singly_implemented = [];
  73. foreach ( $classes as $class => $error_message ) {
  74. if ( \interface_exists( $class, false ) ) {
  75. $interfaces[] = $class;
  76. }
  77. else {
  78. $this->setDefinition( $class, $definition = \unserialize( $serialized_prototype ) );
  79. if ( null !== $error_message ) {
  80. $definition->addError( $error_message );
  81. continue;
  82. }
  83. foreach ( \class_implements( $class, false ) as $interface ) {
  84. $singly_implemented[ $interface ] = isset( $singly_implemented[ $interface ] ) ? false : $class;
  85. }
  86. }
  87. }
  88. foreach ( $interfaces as $interface ) {
  89. if ( ! empty( $singly_implemented[ $interface ] ) ) {
  90. $this->container->setAlias( $interface, $singly_implemented[ $interface ] )
  91. ->setPublic( false );
  92. }
  93. }
  94. }
  95. /**
  96. * Registers a definition in the container with its instanceof-conditionals.
  97. *
  98. * @param string $id The ID of the definition.
  99. * @param \Symfony\Component\DependencyInjection\Definition $definition The definition.
  100. *
  101. * @throws InvalidArgumentException If invalid arguments were supplied.
  102. *
  103. * @return void
  104. */
  105. protected function setDefinition( $id, Definition $definition ) {
  106. $this->container->removeBindings( $id );
  107. // @codingStandardsIgnoreLine WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar This is from an inherited class not abiding by that standard.
  108. if ( $this->isLoadingInstanceof ) {
  109. if ( ! $definition instanceof ChildDefinition ) {
  110. throw new InvalidArgumentException( \sprintf( 'Invalid type definition "%s": ChildDefinition expected, "%s" given.', $id, \get_class( $definition ) ) );
  111. }
  112. $this->instanceof[ $id ] = $definition;
  113. }
  114. else {
  115. $this->container->setDefinition( $id, ( $definition instanceof ChildDefinition ) ? $definition : $definition->setInstanceofConditionals( $this->instanceof ) );
  116. }
  117. }
  118. /**
  119. * Finds classes based on a given pattern and exclude pattern.
  120. *
  121. * @param string $namespace The namespace prefix of classes in the scanned directory.
  122. * @param string $pattern The directory to look for classes, glob-patterns allowed.
  123. * @param string $exclude A globed path of files to exclude.
  124. *
  125. * @throws InvalidArgumentException If invalid arguments were supplied.
  126. *
  127. * @return array The found classes.
  128. */
  129. private function findClasses( $namespace, $pattern, $exclude ) {
  130. $parameter_bag = $this->container->getParameterBag();
  131. $exclude_paths = [];
  132. $exclude_prefix = null;
  133. if ( $exclude ) {
  134. $exclude = $parameter_bag->unescapeValue( $parameter_bag->resolveValue( $exclude ) );
  135. foreach ( $this->glob( $exclude, true, $resource ) as $path => $info ) {
  136. if ( null === $exclude_prefix ) {
  137. $exclude_prefix = $resource->getPrefix();
  138. }
  139. // Normalize Windows slashes.
  140. $exclude_paths[ \str_replace( '\\', '/', $path ) ] = true;
  141. }
  142. }
  143. $pattern = $parameter_bag->unescapeValue( $parameter_bag->resolveValue( $pattern ) );
  144. $classes = [];
  145. $ext_regexp = \defined( 'HHVM_VERSION' ) ? '/\\.(?:php|hh)$/' : '/\\.php$/';
  146. $prefix_len = null;
  147. foreach ( $this->glob( $pattern, true, $resource ) as $path => $info ) {
  148. if ( null === $prefix_len ) {
  149. $prefix_len = \strlen( $resource->getPrefix() );
  150. if ( $exclude_prefix && 0 !== \strpos( $exclude_prefix, $resource->getPrefix() ) ) {
  151. throw new InvalidArgumentException( \sprintf( 'Invalid "exclude" pattern when importing classes for "%s": make sure your "exclude" pattern (%s) is a subset of the "resource" pattern (%s)', $namespace, $exclude, $pattern ) );
  152. }
  153. }
  154. if ( isset( $exclude_paths[ \str_replace( '\\', '/', $path ) ] ) ) {
  155. continue;
  156. }
  157. if ( ! \preg_match( $ext_regexp, $path, $m ) || ! $info->isReadable() ) {
  158. continue;
  159. }
  160. $class = $this->getClassFromClassMap( $path );
  161. if ( ! $class || ! \preg_match( '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+$/', $class ) ) {
  162. continue;
  163. }
  164. try {
  165. $r = $this->container->getReflectionClass( $class );
  166. } catch ( \ReflectionException $e ) {
  167. $classes[ $class ] = \sprintf(
  168. 'While discovering services from namespace "%s", an error was thrown when processing the class "%s": "%s".',
  169. $namespace,
  170. $class,
  171. $e->getMessage()
  172. );
  173. continue;
  174. }
  175. // Check to make sure the expected class exists.
  176. if ( ! $r ) {
  177. throw new InvalidArgumentException( \sprintf( 'Expected to find class "%s" in file "%s" while importing services from resource "%s", but it was not found! Check the namespace prefix used with the resource.', $class, $path, $pattern ) );
  178. }
  179. if ( $r->isInstantiable() || $r->isInterface() ) {
  180. $classes[ $class ] = null;
  181. }
  182. }
  183. // Track only for new & removed files.
  184. if ( $resource instanceof GlobResource ) {
  185. $this->container->addResource( $resource );
  186. }
  187. else {
  188. foreach ( $resource as $path ) {
  189. $this->container->fileExists( $path, false );
  190. }
  191. }
  192. return $classes;
  193. }
  194. }