Magento 2 Documentation  2.3
Documentation for Magento 2 CMS v2.3 (December 2018)
PublicCodeTest.php
Go to the documentation of this file.
1 <?php
6 namespace Magento\Test\Integrity;
7 
9 
13 class PublicCodeTest extends \PHPUnit\Framework\TestCase
14 {
21  private $simpleReturnTypes = [
22  '$this', 'void', 'string', 'int', 'bool', 'boolean', 'integer', 'null'
23  ];
24 
28  private $blockWhitelist;
29 
35  private function getWhitelist(): array
36  {
37  if ($this->blockWhitelist === null) {
38  $whiteListFiles = str_replace(
39  '\\',
40  '/',
41  realpath(__DIR__) . '/_files/whitelist/public_code*.txt'
42  );
43  $whiteListItems = [];
44  foreach (glob($whiteListFiles) as $fileName) {
45  $whiteListItems = array_merge(
46  $whiteListItems,
47  file($fileName, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)
48  );
49  }
50  $this->blockWhitelist = $whiteListItems;
51  }
52  return $this->blockWhitelist;
53  }
54 
63  public function testAllBlocksReferencedInLayoutArePublic($layoutFile)
64  {
65  $nonPublishedBlocks = [];
66  $xml = simplexml_load_file($layoutFile);
67  $elements = $xml->xpath('//block | //referenceBlock') ?: [];
69  foreach ($elements as $node) {
70  $class = (string) $node['class'];
71  if ($class && \class_exists($class) && !in_array($class, $this->getWhitelist())) {
72  $reflection = (new \ReflectionClass($class));
73  if (strpos($reflection->getDocComment(), '@api') === false) {
74  $nonPublishedBlocks[] = $class;
75  }
76  }
77  }
78  if (count($nonPublishedBlocks)) {
79  $this->fail(
80  "Layout file '$layoutFile' uses following blocks that are not marked with @api annotation:\n"
81  . implode(",\n", array_unique($nonPublishedBlocks))
82  );
83  }
84  }
85 
92  public function layoutFilesDataProvider()
93  {
94  return Files::init()->getLayoutFiles([], true);
95  }
96 
108  {
109  $nonPublishedClasses = [];
110  $reflection = new \ReflectionClass($class);
111  $filter = \ReflectionMethod::IS_PUBLIC;
112  if ($reflection->isAbstract()) {
113  $filter = $filter | \ReflectionMethod::IS_PROTECTED;
114  }
115  $methods = $reflection->getMethods($filter);
116  foreach ($methods as $method) {
117  if ($method->isConstructor()) {
118  continue;
119  }
120  $nonPublishedClasses = $this->checkParameters($class, $method, $nonPublishedClasses);
121  /* Taking into account docblock return types since this code
122  is written on early php 7 when return types are not actively used */
123  $returnTypes = [];
124  if ($method->hasReturnType()) {
125  if (!$method->getReturnType()->isBuiltin()) {
126  $returnTypes = [trim($method->getReturnType()->__toString(), '?[]')];
127  }
128  } else {
129  $returnTypes = $this->getReturnTypesFromDocComment($method->getDocComment());
130  }
131  $nonPublishedClasses = $this->checkReturnValues($class, $returnTypes, $nonPublishedClasses);
132  }
133 
134  if (count($nonPublishedClasses)) {
135  $this->fail(
136  "Public type '" . $class . "' references following non-public types:\n"
137  . implode("\n", array_unique($nonPublishedClasses))
138  );
139  }
140  }
141 
147  public function publicPHPTypesDataProvider()
148  {
150  $result = [];
151  foreach ($files as $file) {
152  $fileContents = \file_get_contents($file);
153  if (strpos($fileContents, '@api') !== false) {
154  foreach ($this->getDeclaredClassesAndInterfaces($file) as $class) {
155  if (!in_array($class->getName(), $this->getWhitelist())
156  && (class_exists($class->getName()) || interface_exists($class->getName()))
157  ) {
158  $result[$class->getName()] = [$class->getName()];
159  }
160  }
161  }
162  }
163  return $result;
164  }
165 
172  private function getDeclaredClassesAndInterfaces($file)
173  {
174  $fileScanner = new \Magento\Setup\Module\Di\Code\Reader\FileScanner($file);
175  return $fileScanner->getClasses();
176  }
177 
184  private function isPublished(\ReflectionClass $class)
185  {
186  return strpos($class->getDocComment(), '@api') !== false;
187  }
188 
196  private function areClassesFromSameVendor($classNameA, $classNameB)
197  {
198  $classNameA = ltrim($classNameA, '\\');
199  $classNameB = ltrim($classNameB, '\\');
200  $aVendor = substr($classNameA, 0, strpos($classNameA, '\\'));
201  $bVendor = substr($classNameB, 0, strpos($classNameB, '\\'));
202  return $aVendor === $bVendor;
203  }
204 
213  private function isGenerated($className)
214  {
215  return substr($className, -18) === 'ExtensionInterface' || substr($className, -7) === 'Factory';
216  }
217 
226  private function getReturnTypesFromDocComment($docComment)
227  {
228  // TODO: add docblock namespace resolving using third-party library
229  if (preg_match('/@return (\S*)/', $docComment, $matches)) {
230  return array_map(
231  'trim',
232  explode('|', $matches[1])
233  );
234  } else {
235  return [];
236  }
237  }
238 
249  private function checkReturnValues($class, array $returnTypes, array $nonPublishedClasses)
250  {
251  foreach ($returnTypes as $returnType) {
252  if (!in_array($returnType, $this->simpleReturnTypes)
253  && !$this->isGenerated($returnType)
254  && \class_exists($returnType)
255  ) {
256  $returnTypeReflection = new \ReflectionClass($returnType);
257  if (!$returnTypeReflection->isInternal()
258  && $this->areClassesFromSameVendor($returnType, $class)
259  && !$this->isPublished($returnTypeReflection)
260  ) {
261  $nonPublishedClasses[$returnType] = $returnType;
262  }
263  }
264  }
265  return $nonPublishedClasses;
266  }
267 
275  private function checkParameters($class, \ReflectionMethod $method, array $nonPublishedClasses)
276  {
277  /* Ignoring docblocks for argument types */
278  foreach ($method->getParameters() as $parameter) {
279  if ($parameter->hasType()
280  && !$parameter->getType()->isBuiltin()
281  && !$this->isGenerated($parameter->getType()->__toString())
282  ) {
283  $parameterClass = $parameter->getClass();
284  /*
285  * We don't want to check integrity of @api coverage of classes
286  * that belong to different vendors, because it is too complicated.
287  * Example:
288  * If Magento class references non-@api annotated class from Zend,
289  * we don't want to fail test, because Zend is considered public by default,
290  * and we don't care if Zend classes are @api-annotated
291  */
292  if (!$parameterClass->isInternal()
293  && $this->areClassesFromSameVendor($parameterClass->getName(), $class)
294  && !$this->isPublished($parameterClass)
295  ) {
296  $nonPublishedClasses[$parameterClass->getName()] = $parameterClass->getName();
297  }
298  }
299  }
300  return $nonPublishedClasses;
301  }
302 }
defined('TESTS_BP')||define('TESTS_BP' __DIR__
Definition: _bootstrap.php:60
$fileName
Definition: translate.phtml:15
$methods
Definition: billing.phtml:71
$_option $_optionId $class
Definition: date.phtml:13
$method
Definition: info.phtml:13
foreach($appDirs as $dir) $files
if($currentSelectedMethod==$_code) $className
Definition: form.phtml:31