From 7aa586d38abb46aef98f5820af760f66b078dc52 Mon Sep 17 00:00:00 2001
From: Bizley <pawel@positive.codes>
Date: Mon, 30 Aug 2021 15:20:46 +0200
Subject: [PATCH] Draft for 3.0

---
 .gitignore                               |  4 +-
 commands/ApiController.php               |  3 +-
 composer.json                            | 31 +++-------
 models/BaseDoc.php                       | 27 ++++++---
 models/ClassDoc.php                      |  4 +-
 models/Context.php                       | 77 +++++++++++++++++++-----
 models/FunctionDoc.php                   | 15 +++--
 models/PropertyDoc.php                   | 13 ++--
 models/TypeDoc.php                       |  4 +-
 renderers/BaseRenderer.php               | 31 ++++------
 templates/bootstrap/RendererTrait.php    |  2 +-
 templates/html/views/methodSummary.php   |  4 +-
 templates/html/views/propertySummary.php |  2 +-
 tests/TestCase.php                       |  6 +-
 tests/commands/ApiControllerTest.php     | 18 +++---
 tests/commands/GuideControllerTest.php   | 18 +++---
 16 files changed, 155 insertions(+), 104 deletions(-)

diff --git a/.gitignore b/.gitignore
index 803a6f0d..f2cdbfd5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,4 +32,6 @@ phpunit.phar
 /tests/runtime
 
 # local tests configuration
-/tests/data/config.local.php
\ No newline at end of file
+/tests/data/config.local.php
+
+.phpunit.result.cache
diff --git a/commands/ApiController.php b/commands/ApiController.php
index e2484385..d1e4259b 100644
--- a/commands/ApiController.php
+++ b/commands/ApiController.php
@@ -97,7 +97,7 @@ public function actionIndex(array $sourceDirs, $targetDir)
         $done = 0;
         foreach ($files as $file) {
             try {
-                $context->addFile($file);
+                $context->addProjectFile($file);
             } catch (\Exception $e) {
                 $context->errors[] = "Unable to process \"$file\": " . $e->getMessage();
             }
@@ -105,6 +105,7 @@ public function actionIndex(array $sourceDirs, $targetDir)
         }
         Console::endProgress(true);
         $this->stdout('done.' . PHP_EOL, Console::FG_GREEN);
+        $context->processFiles();
 
         // save processed data to cache
         $this->storeContext($context, $targetDir);
diff --git a/composer.json b/composer.json
index 9ba468fc..5d0a74b1 100644
--- a/composer.json
+++ b/composer.json
@@ -19,20 +19,19 @@
     ],
     "minimum-stability": "dev",
     "require": {
-        "php": ">=5.4",
-        "yiisoft/yii2": "~2.0.13",
+        "php": "^7.2",
+        "yiisoft/yii2": "~2.0.16",
         "yiisoft/yii2-bootstrap": "~2.0.0",
-        "phpdocumentor/reflection": "^3.0.1",
-        "phpdocumentor/reflection-docblock": "^2.0.4",
-        "nikic/php-parser": "^1.0",
-        "cebe/js-search": "~0.9.3",
-        "cebe/markdown": "~1.0.0 | ~1.1.0 | ~1.2.0",
-        "cebe/markdown-latex": "~1.0",
-        "scrivo/highlight.php": "~9.13"
+        "phpdocumentor/reflection": "^4.0 | ^5.0",
+        "phpdocumentor/reflection-docblock": "^4.0 | ^5.0",
+        "nikic/php-parser": "^4.0",
+        "cebe/js-search": "~0.9.0",
+        "cebe/markdown": "^1.0",
+        "cebe/markdown-latex": "^1.0",
+        "scrivo/highlight.php": "^9.0"
     },
     "require-dev": {
-        "cweagans/composer-patches": "^1.7",
-        "phpunit/phpunit": "4.8.34"
+        "phpunit/phpunit": "*"
     },
     "repositories": [
         {
@@ -47,16 +46,6 @@
     "extra": {
         "branch-alias": {
             "dev-master": "2.1.x-dev"
-        },
-        "composer-exit-on-patch-failure": true,
-        "patches": {
-            "phpunit/phpunit-mock-objects": {
-                "Fix PHP 7 and 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_mock_objects.patch"
-            },
-            "phpunit/phpunit": {
-                "Fix PHP 7 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php7.patch",
-                "Fix PHP 8 compatibility": "https://yiisoft.github.io/phpunit-patches/phpunit_php8.patch"
-            }
         }
     }
 }
diff --git a/models/BaseDoc.php b/models/BaseDoc.php
index 3f4d94fe..5ed7a2a5 100644
--- a/models/BaseDoc.php
+++ b/models/BaseDoc.php
@@ -10,6 +10,10 @@
 use phpDocumentor\Reflection\DocBlock\Tag;
 use phpDocumentor\Reflection\DocBlock\Tag\DeprecatedTag;
 use phpDocumentor\Reflection\DocBlock\Tag\SinceTag;
+use phpDocumentor\Reflection\DocBlock\Tags\Deprecated;
+use phpDocumentor\Reflection\DocBlock\Tags\Generic;
+use phpDocumentor\Reflection\DocBlock\Tags\Since;
+use phpDocumentor\Reflection\Php\Class_;
 use yii\base\BaseObject;
 use yii\helpers\StringHelper;
 
@@ -26,6 +30,7 @@ class BaseDoc extends BaseObject
      */
     public $phpDocContext;
     public $name;
+    public $shortName;
     public $sourceFile;
     public $startLine;
     public $endLine;
@@ -101,7 +106,7 @@ public function getPackageName()
     }
 
     /**
-     * @param \phpDocumentor\Reflection\BaseReflector $reflector
+     * @param Class_ $reflector
      * @param Context $context
      * @param array $config
      */
@@ -114,13 +119,17 @@ public function __construct($reflector = null, $context = null, $config = [])
         }
 
         // base properties
-        $this->name = ltrim($reflector->getName(), '\\');
-        $this->startLine = $reflector->getNode()->getAttribute('startLine');
-        $this->endLine = $reflector->getNode()->getAttribute('endLine');
+        $this->name = ltrim((string) $reflector->getFqsen(), '\\');
+        $separator = strrpos($this->name, '::');
+        if ($separator !== false) {
+            $this->shortName = substr($this->name, $separator + 2);
+        }
+        $this->startLine = $reflector->getLocation()->getLineNumber();
+        //$this->endLine = $reflector->getNode()->getAttribute('endLine');
 
         $docblock = $reflector->getDocBlock();
         if ($docblock !== null) {
-            $this->shortDescription = static::mbUcFirst($docblock->getShortDescription());
+            $this->shortDescription = StringHelper::mb_ucfirst($docblock->getSummary());
             if (empty($this->shortDescription) && !($this instanceof PropertyDoc) && $context !== null && $docblock->getTagsByName('inheritdoc') === null) {
                 $context->warnings[] = [
                     'line' => $this->startLine,
@@ -128,16 +137,16 @@ public function __construct($reflector = null, $context = null, $config = [])
                     'message' => "No short description for " . substr(StringHelper::basename(get_class($this)), 0, -3) . " '{$this->name}'",
                 ];
             }
-            $this->description = $docblock->getLongDescription()->getContents();
+            $this->description = $docblock->getDescription()->render();
 
             $this->phpDocContext = $docblock->getContext();
 
             $this->tags = $docblock->getTags();
             foreach ($this->tags as $i => $tag) {
-                if ($tag instanceof SinceTag) {
+                if ($tag instanceof Since) {
                     $this->since = $tag->getVersion();
                     unset($this->tags[$i]);
-                } elseif ($tag instanceof DeprecatedTag) {
+                } elseif ($tag instanceof Deprecated) {
                     $this->deprecatedSince = $tag->getVersion();
                     $this->deprecatedReason = $tag->getDescription();
                     unset($this->tags[$i]);
@@ -147,7 +156,7 @@ public function __construct($reflector = null, $context = null, $config = [])
             if ($this->shortDescription === '{@inheritdoc}') {
                 // Mock up parsing of '{@inheritdoc}' (in brackets) tag, which is not yet supported at "phpdocumentor/reflection-docblock" 2.x
                 // todo consider removal in case of "phpdocumentor/reflection-docblock" upgrade
-                $this->tags[] = new Tag('inheritdoc', '');
+                $this->tags[] = new Generic('inheritdoc');
                 $this->shortDescription = '';
             }
 
diff --git a/models/ClassDoc.php b/models/ClassDoc.php
index 8e9a12bf..813b1ce7 100644
--- a/models/ClassDoc.php
+++ b/models/ClassDoc.php
@@ -101,7 +101,7 @@ public function __construct($reflector = null, $context = null, $config = [])
             return;
         }
 
-        $this->parentClass = ltrim($reflector->getParentClass(), '\\');
+        $this->parentClass = ltrim($reflector->getParent(), '\\');
         if (empty($this->parentClass)) {
             $this->parentClass = null;
         }
@@ -111,7 +111,7 @@ public function __construct($reflector = null, $context = null, $config = [])
         foreach ($reflector->getInterfaces() as $interface) {
             $this->interfaces[] = ltrim($interface, '\\');
         }
-        foreach ($reflector->getTraits() as $trait) {
+        foreach ($reflector->getUsedTraits() as $trait) {
             $this->traits[] = ltrim($trait, '\\');
         }
         foreach ($reflector->getConstants() as $constantReflector) {
diff --git a/models/Context.php b/models/Context.php
index 24dea163..3f201640 100644
--- a/models/Context.php
+++ b/models/Context.php
@@ -5,15 +5,18 @@
  * @license http://www.yiiframework.com/license/
  */
 
+declare(strict_types=1);
+
 namespace yii\apidoc\models;
 
-use phpDocumentor\Reflection\FileReflector;
+use phpDocumentor\Reflection\File\LocalFile;
+use phpDocumentor\Reflection\Php\Project;
+use phpDocumentor\Reflection\Php\ProjectFactory;
 use yii\base\Component;
 
 /**
- *
  * @author Carsten Brandt <mail@cebe.cc>
- * @since 2.0
+ * @author Paweł Brzozowski <pawel@positive.codes>
  */
 class Context extends Component
 {
@@ -39,7 +42,6 @@ class Context extends Component
     public $errors = [];
     /**
      * @var array
-     * @since 2.0.6
      */
     public $warnings = [];
 
@@ -49,29 +51,58 @@ class Context extends Component
      * @param string $type
      * @return null|ClassDoc|InterfaceDoc|TraitDoc
      */
-    public function getType($type)
+    public function getType(string $type)
     {
         $type = ltrim($type, '\\');
+
         if (isset($this->classes[$type])) {
             return $this->classes[$type];
-        } elseif (isset($this->interfaces[$type])) {
+        }
+        if (isset($this->interfaces[$type])) {
             return $this->interfaces[$type];
-        } elseif (isset($this->traits[$type])) {
+        }
+        if (isset($this->traits[$type])) {
             return $this->traits[$type];
         }
 
         return null;
     }
 
+    public function addProjectFile($fileName): void
+    {
+        $this->files[$fileName] = sha1_file($fileName);
+    }
+
+    public function processFiles(): void
+    {
+        $projectFiles = $this->getReflectionProject()->getFiles();
+        foreach ($this->files as $fileName => $hash) {
+            $reflection = $projectFiles[$fileName];
+
+            foreach ($reflection->getClasses() as $class) {
+                $class = new ClassDoc($class, $this, ['sourceFile' => $fileName]);
+                $this->classes[$class->name] = $class;
+            }
+            foreach ($reflection->getInterfaces() as $interface) {
+                $interface = new InterfaceDoc($interface, $this, ['sourceFile' => $fileName]);
+                $this->interfaces[$interface->name] = $interface;
+            }
+            foreach ($reflection->getTraits() as $trait) {
+                $trait = new TraitDoc($trait, $this, ['sourceFile' => $fileName]);
+                $this->traits[$trait->name] = $trait;
+            }
+        }
+    }
+
     /**
      * Adds file to context
      * @param string $fileName
      */
-    public function addFile($fileName)
+    public function addFile(string $fileName): void
     {
         $this->files[$fileName] = sha1_file($fileName);
 
-        $reflection = new FileReflector($fileName, true);
+        $reflection = new \phpDocumentor\Reflection\File\LocalFile($fileName, true);
         $reflection->process();
 
         foreach ($reflection->getClasses() as $class) {
@@ -91,7 +122,7 @@ public function addFile($fileName)
     /**
      * Updates references
      */
-    public function updateReferences()
+    public function updateReferences(): void
     {
         // update all subclass references
         foreach ($this->classes as $class) {
@@ -101,10 +132,12 @@ public function updateReferences()
                 $class->subclasses[] = $className;
             }
         }
+
         // update interfaces of subclasses
         foreach ($this->classes as $class) {
             $this->updateSubclassInterfacesTraits($class);
         }
+
         // update implementedBy and usedBy for interfaces and traits
         foreach ($this->classes as $class) {
             foreach ($class->traits as $trait) {
@@ -306,7 +339,7 @@ private function inheritMethodRecursive($method, $class)
         foreach($inheritanceCandidates as $candidate) {
             if (isset($candidate->methods[$method->name])) {
                 $cmethod = $candidate->methods[$method->name];
-                if ($cmethod->hasTag('inheritdoc')) {
+                if (!$candidate instanceof InterfaceDoc && $cmethod->hasTag('inheritdoc')) {
                     $this->inheritDocs($candidate);
                 }
                 $methods[] = $cmethod;
@@ -458,7 +491,7 @@ private function hasNonOptionalParams($method, $number = 0)
                 $count++;
             }
         }
-        return $count == $number;
+        return $count === $number;
     }
 
     /**
@@ -485,15 +518,31 @@ protected function isSubclassOf($classA, $classB)
         if (is_object($classB)) {
             $classB = $classB->name;
         }
-        if ($classA->name == $classB) {
+        if ($classA->name === $classB) {
             return true;
         }
         while ($classA->parentClass !== null && isset($this->classes[$classA->parentClass])) {
             $classA = $this->classes[$classA->parentClass];
-            if ($classA->name == $classB) {
+            if ($classA->name === $classB) {
                 return true;
             }
         }
         return false;
     }
+
+    private $reflectionProject;
+
+    public function getReflectionProject(): Project
+    {
+        if ($this->reflectionProject === null) {
+            $files = [];
+            foreach ($this->files as $fileName => $hash) {
+                $files[] = new LocalFile($fileName);
+            }
+
+            $this->reflectionProject = ProjectFactory::createInstance()->create('ApiDoc', $files);
+        }
+
+        return $this->reflectionProject;
+    }
 }
diff --git a/models/FunctionDoc.php b/models/FunctionDoc.php
index 728f4e25..0242ae8c 100644
--- a/models/FunctionDoc.php
+++ b/models/FunctionDoc.php
@@ -11,6 +11,11 @@
 use phpDocumentor\Reflection\DocBlock\Tag\PropertyTag;
 use phpDocumentor\Reflection\DocBlock\Tag\ReturnTag;
 use phpDocumentor\Reflection\DocBlock\Tag\ThrowsTag;
+use phpDocumentor\Reflection\DocBlock\Tags\Param;
+use phpDocumentor\Reflection\DocBlock\Tags\Throws;
+use phpDocumentor\Reflection\Php\Function_;
+use phpDocumentor\Reflection\Php\Method;
+use phpDocumentor\Reflection\Php\Property;
 
 /**
  * Represents API documentation information for a `function`.
@@ -32,7 +37,7 @@ class FunctionDoc extends BaseDoc
 
 
     /**
-     * @param \phpDocumentor\Reflection\FunctionReflector $reflector
+     * @param Method $reflector
      * @param Context $context
      * @param array $config
      */
@@ -44,7 +49,7 @@ public function __construct($reflector = null, $context = null, $config = [])
             return;
         }
 
-        $this->isReturnByReference = $reflector->isByRef();
+        $this->isReturnByReference = false;//$reflector->isByRef();
 
         foreach ($reflector->getArguments() as $arg) {
             $arg = new ParamDoc($arg, $context, ['sourceFile' => $this->sourceFile]);
@@ -52,12 +57,12 @@ public function __construct($reflector = null, $context = null, $config = [])
         }
 
         foreach ($this->tags as $i => $tag) {
-            if ($tag instanceof ThrowsTag) {
+            if ($tag instanceof Throws) {
                 $this->exceptions[$tag->getType()] = $tag->getDescription();
                 unset($this->tags[$i]);
-            } elseif ($tag instanceof PropertyTag) {
+            } elseif ($tag instanceof Property) {
                 // ignore property tag
-            } elseif ($tag instanceof ParamTag) {
+            } elseif ($tag instanceof Param) {
                 $paramName = $tag->getVariableName();
                 if (!isset($this->params[$paramName]) && $context !== null) {
                     $context->errors[] = [
diff --git a/models/PropertyDoc.php b/models/PropertyDoc.php
index 4a61c01b..8552fa28 100644
--- a/models/PropertyDoc.php
+++ b/models/PropertyDoc.php
@@ -7,7 +7,8 @@
 
 namespace yii\apidoc\models;
 
-use phpDocumentor\Reflection\DocBlock\Tag\VarTag;
+use phpDocumentor\Reflection\DocBlock\Tags\Var_;
+use phpDocumentor\Reflection\Php\Property;
 use yii\apidoc\helpers\PrettyPrinter;
 
 /**
@@ -50,7 +51,7 @@ public function getIsWriteOnly()
     }
 
     /**
-     * @param \phpDocumentor\Reflection\ClassReflector\PropertyReflector $reflector
+     * @param Property $reflector
      * @param Context $context
      * @param array $config
      */
@@ -66,8 +67,8 @@ public function __construct($reflector = null, $context = null, $config = [])
         $this->isStatic = $reflector->isStatic();
 
         // bypass $reflector->getDefault() for short array syntax
-        if ($reflector->getNode()->default) {
-            $this->defaultValue = PrettyPrinter::getRepresentationOfValue($reflector->getNode()->default);
+        if ($reflector->getDefault()) {
+            $this->defaultValue = $reflector->getDefault(); //PrettyPrinter::getRepresentationOfValue($reflector->getDefault());
         }
 
         $hasInheritdoc = false;
@@ -75,9 +76,9 @@ public function __construct($reflector = null, $context = null, $config = [])
             if ($tag->getName() === 'inheritdoc') {
                 $hasInheritdoc = true;
             }
-            if ($tag instanceof VarTag) {
+            if ($tag instanceof Var_) {
                 $this->type = $tag->getType();
-                $this->types = $tag->getTypes();
+                $this->types = [$tag->getType()];
                 $this->description = static::mbUcFirst($tag->getDescription());
                 $this->shortDescription = BaseDoc::extractFirstSentence($this->description);
             }
diff --git a/models/TypeDoc.php b/models/TypeDoc.php
index 97e283ba..a69fc03a 100644
--- a/models/TypeDoc.php
+++ b/models/TypeDoc.php
@@ -250,7 +250,7 @@ public function __construct($reflector = null, $context = null, $config = [])
         }
 
         foreach ($reflector->getProperties() as $propertyReflector) {
-            if ($propertyReflector->getVisibility() != 'private') {
+            if ($propertyReflector->getVisibility() !== 'private') {
                 $property = new PropertyDoc($propertyReflector, $context, ['sourceFile' => $this->sourceFile]);
                 $property->definedBy = $this->name;
                 $this->properties[$property->name] = $property;
@@ -258,7 +258,7 @@ public function __construct($reflector = null, $context = null, $config = [])
         }
 
         foreach ($reflector->getMethods() as $methodReflector) {
-            if ($methodReflector->getVisibility() != 'private') {
+            if ($methodReflector->getVisibility() !== 'private') {
                 $method = new MethodDoc($methodReflector, $context, ['sourceFile' => $this->sourceFile]);
                 $method->definedBy = $this->name;
                 $this->methods[$method->name] = $method;
diff --git a/renderers/BaseRenderer.php b/renderers/BaseRenderer.php
index b2f55553..821323b5 100644
--- a/renderers/BaseRenderer.php
+++ b/renderers/BaseRenderer.php
@@ -95,6 +95,11 @@ public function createTypeLink($types, $context = null, $title = null, $options
                     ltrim($type, '\\');
                 }
             }
+
+            if (is_object($type) && \method_exists($type, '__toString')) {
+                $type = (string)$type;
+            }
+
             if (is_string($type)) {
                 $linkText = ltrim($type, '\\');
                 if ($title !== null) {
@@ -157,32 +162,22 @@ public function createTypeLink($types, $context = null, $title = null, $options
     /**
      * creates a link to a subject
      * @param PropertyDoc|MethodDoc|ConstDoc|EventDoc $subject
-     * @param string $title
+     * @param string|null $title
      * @param array $options additional HTML attributes for the link.
      * @return string
      */
-    public function createSubjectLink($subject, $title = null, $options = [])
+    public function createSubjectLink($subject, string $title = null, array $options = []): string
     {
         if ($title === null) {
-            if ($subject instanceof MethodDoc) {
-                $title = $subject->name . '()';
-            } else {
-                $title = $subject->name;
-            }
+            $title = $subject->shortName;
         }
         if (($type = $this->apiContext->getType($subject->definedBy)) === null) {
-            return $subject->name;
-        } else {
-            $link = $this->generateApiUrl($type->name);
-            if ($subject instanceof MethodDoc) {
-                $link .= '#' . $subject->name . '()';
-            } else {
-                $link .= '#' . $subject->name;
-            }
-            $link .= '-detail';
-
-            return $this->generateLink($title, $link, $options);
+            return $subject->shortName;
         }
+
+        $link = $this->generateApiUrl($type->name) . '#' . $subject->shortName . '-detail';
+
+        return $this->generateLink($title, $link, $options);
     }
 
     /**
diff --git a/templates/bootstrap/RendererTrait.php b/templates/bootstrap/RendererTrait.php
index 2ec72bbd..c184b838 100644
--- a/templates/bootstrap/RendererTrait.php
+++ b/templates/bootstrap/RendererTrait.php
@@ -100,7 +100,7 @@ protected function filterTypes($types, $navClasses)
             case 'yii':
                 $self = $this;
                 $types = array_filter($types, function ($val) use ($self) {
-                    if ($val->name == 'Yii' || $val->name == 'YiiRequirementChecker') {
+                    if ($val->name === 'Yii' || $val->name === 'YiiRequirementChecker') {
                         return true;
                     }
                     if (strlen($val->name) < 5) {
diff --git a/templates/html/views/methodSummary.php b/templates/html/views/methodSummary.php
index 170cdf13..04565ef8 100644
--- a/templates/html/views/methodSummary.php
+++ b/templates/html/views/methodSummary.php
@@ -36,8 +36,8 @@
 ArrayHelper::multisort($methods, 'name');
 foreach ($methods as $method): ?>
     <?php if ($protected && $method->visibility == 'protected' || !$protected && $method->visibility != 'protected'): ?>
-    <tr<?= $method->definedBy != $type->name ? ' class="inherited"' : '' ?> id="<?= $method->name ?>()">
-        <td><?= $renderer->createSubjectLink($method, $method->name.'()') ?></td>
+    <tr<?= $method->definedBy != $type->name ? ' class="inherited"' : '' ?> id="<?= $method->shortName ?>">
+        <td><?= $renderer->createSubjectLink($method) ?></td>
         <td><?= ApiMarkdown::process($method->shortDescription, $method->definedBy, true) ?></td>
         <td><?= $renderer->createTypeLink($method->definedBy, $type) ?></td>
     </tr>
diff --git a/templates/html/views/propertySummary.php b/templates/html/views/propertySummary.php
index 45e373a3..c4051a7e 100644
--- a/templates/html/views/propertySummary.php
+++ b/templates/html/views/propertySummary.php
@@ -36,7 +36,7 @@
 ArrayHelper::multisort($properties, 'name');
 foreach ($properties as $property): ?>
     <?php if ($protected && $property->visibility == 'protected' || !$protected && $property->visibility != 'protected'): ?>
-    <tr<?= $property->definedBy != $type->name ? ' class="inherited"' : '' ?> id="<?= $property->name ?>">
+    <tr<?= $property->definedBy !== $type->name ? ' class="inherited"' : '' ?> id="<?= $property->shortName ?>">
         <td><?= $renderer->createSubjectLink($property) ?></td>
         <td><?= $renderer->createTypeLink($property->types) ?></td>
         <td><?= ApiMarkdown::process($property->shortDescription, $property->definedBy, true) ?></td>
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 3153dd6d..b55e8882 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -16,7 +16,7 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase
      * Clean up after test.
      * By default the application created with [[mockApplication]] will be destroyed.
      */
-    protected function tearDown()
+    protected function tearDown(): void
     {
         parent::tearDown();
         $this->removeRuntimeDirectory();
@@ -71,11 +71,11 @@ public function assertEqualsWithoutLE($expected, $actual)
      * @param mixed  $haystack
      * @param string $message
      */
-    public function assertContainsWithoutIndent($needle, $haystack, $message = '')
+    public function assertContainsWithoutIndent($needle, $haystack/*, $message = ''*/)
     {
         $needle = str_replace(["\r", "\n", "\t", '  '], '', $needle);
         $haystack = str_replace(["\r", "\n", "\t", '  '], '', $haystack);
-        $this->assertContains($needle, $haystack, $message);
+        $this->assertStringContainsString($needle, $haystack/*, $message*/);
     }
 
     /**
diff --git a/tests/commands/ApiControllerTest.php b/tests/commands/ApiControllerTest.php
index 1fda17d9..0acece92 100644
--- a/tests/commands/ApiControllerTest.php
+++ b/tests/commands/ApiControllerTest.php
@@ -19,7 +19,7 @@ class ApiControllerTest extends TestCase
     /**
      * {@inheritdoc}
      */
-    protected function setUp()
+    protected function setUp(): void
     {
         parent::setUp();
         $this->mockApplication();
@@ -56,7 +56,7 @@ public function testNoFiles()
         $output = $this->generateApi(Yii::getAlias('@yiiunit/apidoc/data/guide'));
 
         $this->assertNotEmpty($output);
-        $this->assertContains('Error: No files found to process', $output);
+        $this->assertStringContainsString('Error: No files found to process', $output);
     }
 
     public function testGenerateBootstrap()
@@ -64,7 +64,7 @@ public function testGenerateBootstrap()
         $output = $this->generateApi(Yii::getAlias('@yiiunit/apidoc/data/api'), '@runtime', ['template' => 'bootstrap']);
 
         $this->assertNotEmpty($output);
-        $this->assertContains('generating search index...done.', $output);
+        $this->assertStringContainsString('generating search index...done.', $output);
 
         $outputPath = Yii::getAlias('@runtime');
 
@@ -72,9 +72,9 @@ public function testGenerateBootstrap()
         $animalFile = $outputPath . DIRECTORY_SEPARATOR . 'yiiunit-apidoc-data-api-animal-animal.html';
         $this->assertTrue(file_exists($animalFile));
         $animalContent = file_get_contents($animalFile);
-        $this->assertContains('<h1>Abstract Class yiiunit\apidoc\data\api\animal\Animal</h1>', $animalContent);
-        $this->assertContains('<th>Available since version</th><td>1.0</td>', $animalContent);
-        $this->assertContains('Animal is a base class for animals.', $animalContent);
+        $this->assertStringContainsString('<h1>Abstract Class yiiunit\apidoc\data\api\animal\Animal</h1>', $animalContent);
+        $this->assertStringContainsString('<th>Available since version</th><td>1.0</td>', $animalContent);
+        $this->assertStringContainsString('Animal is a base class for animals.', $animalContent);
         $this->assertContainsWithoutIndent(
             <<<HTML
 <tr id="\$name">
@@ -123,13 +123,13 @@ public function testGenerateBootstrap()
         $dogFile = $outputPath . DIRECTORY_SEPARATOR . 'yiiunit-apidoc-data-api-animal-dog.html';
         $this->assertTrue(file_exists($dogFile));
         $dogContent = file_get_contents($dogFile);
-        $this->assertContains('<th>Available since version</th><td>1.1</td>', $dogContent);
-        $this->assertNotContains('@inheritdoc', $dogContent);
+        $this->assertStringContainsString('<th>Available since version</th><td>1.1</td>', $dogContent);
+        $this->assertStringNotContainsString('@inheritdoc', $dogContent);
 
         // Class `Cat` :
         $catFile = $outputPath . DIRECTORY_SEPARATOR . 'yiiunit-apidoc-data-api-animal-cat.html';
         $this->assertTrue(file_exists($catFile));
         $catContent = file_get_contents($catFile);
-        $this->assertNotContains('@inheritdoc', $catContent);
+        $this->assertStringNotContainsString('@inheritdoc', $catContent);
     }
 }
diff --git a/tests/commands/GuideControllerTest.php b/tests/commands/GuideControllerTest.php
index 99d1ca1d..9e6be3ae 100644
--- a/tests/commands/GuideControllerTest.php
+++ b/tests/commands/GuideControllerTest.php
@@ -19,7 +19,7 @@ class GuideControllerTest extends TestCase
     /**
      * {@inheritdoc}
      */
-    protected function setUp()
+    protected function setUp(): void
     {
         parent::setUp();
         $this->mockApplication();
@@ -56,7 +56,7 @@ public function testNoFiles()
         $output = $this->generateGuide(Yii::getAlias('@yiiunit/apidoc/support'));
 
         $this->assertNotEmpty($output);
-        $this->assertContains('Error: No files found to process', $output);
+        $this->assertStringContainsString('Error: No files found to process', $output);
     }
 
     public function testGenerateBootstrap()
@@ -64,23 +64,23 @@ public function testGenerateBootstrap()
         $output = $this->generateGuide(Yii::getAlias('@yiiunit/apidoc/data/guide'), '@runtime', ['template' => 'bootstrap']);
 
         $this->assertNotEmpty($output);
-        $this->assertContains('generating search index...done.', $output);
-        $this->assertContains('Publishing images...done.', $output);
+        $this->assertStringContainsString('generating search index...done.', $output);
+        $this->assertStringContainsString('Publishing images...done.', $output);
 
         $outputPath = Yii::getAlias('@runtime');
 
         $readmeFile = $outputPath . DIRECTORY_SEPARATOR . 'guide-README.html';
         $this->assertTrue(file_exists($readmeFile));
         $readmeContent = file_get_contents($readmeFile);
-        $this->assertContains('<h1>The Test Guide <span id="the-test-guide"></span><a href="#the-test-guide" class="hashlink">', $readmeContent);
-        $this->assertContains('<a href="guide-intro.html">Intro</a>', $readmeContent);
-        $this->assertContains('<a href="guide-intro-upgrade.html">Upgrade</a>', $readmeContent);
+        $this->assertStringContainsString('<h1>The Test Guide <span id="the-test-guide"></span><a href="#the-test-guide" class="hashlink">', $readmeContent);
+        $this->assertStringContainsString('<a href="guide-intro.html">Intro</a>', $readmeContent);
+        $this->assertStringContainsString('<a href="guide-intro-upgrade.html">Upgrade</a>', $readmeContent);
 
         $tocFile = $outputPath . DIRECTORY_SEPARATOR . 'guide-TOC.html';
         $this->assertTrue(file_exists($tocFile));
         $tocFile = file_get_contents($tocFile);
 
-        $this->assertContains('<h1>TOC Test <span id="toc-test"></span><a href="#toc-test" class="hashlink">', $tocFile);
+        $this->assertStringContainsString('<h1>TOC Test <span id="toc-test"></span><a href="#toc-test" class="hashlink">', $tocFile);
         $this->assertEquals(1, substr_count($tocFile, '<div class="toc">'));
     }
 
@@ -89,7 +89,7 @@ public function testGeneratePdf()
         $output = $this->generateGuide(Yii::getAlias('@yiiunit/apidoc/data/guide'), '@runtime', ['template' => 'pdf']);
 
         $this->assertNotEmpty($output);
-        $this->assertContains('Publishing images...done.', $output);
+        $this->assertStringContainsString('Publishing images...done.', $output);
 
         $outputPath = Yii::getAlias('@runtime');
         $this->assertTrue(file_exists($outputPath . DIRECTORY_SEPARATOR . 'Makefile'));
