1<?php
2
3declare(strict_types=1);
4
5/**
6 * This file is part of phpDocumentor.
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 *
11 * @link      http://phpdoc.org
12 */
13
14namespace phpDocumentor\Reflection\DocBlock;
15
16use phpDocumentor\Reflection\DocBlock\Tags\Example;
17use function array_slice;
18use function file;
19use function getcwd;
20use function implode;
21use function is_readable;
22use function rtrim;
23use function sprintf;
24use function trim;
25use const DIRECTORY_SEPARATOR;
26
27/**
28 * Class used to find an example file's location based on a given ExampleDescriptor.
29 */
30class ExampleFinder
31{
32    /** @var string */
33    private $sourceDirectory = '';
34
35    /** @var string[] */
36    private $exampleDirectories = [];
37
38    /**
39     * Attempts to find the example contents for the given descriptor.
40     */
41    public function find(Example $example) : string
42    {
43        $filename = $example->getFilePath();
44
45        $file = $this->getExampleFileContents($filename);
46        if (!$file) {
47            return sprintf('** File not found : %s **', $filename);
48        }
49
50        return implode('', array_slice($file, $example->getStartingLine() - 1, $example->getLineCount()));
51    }
52
53    /**
54     * Registers the project's root directory where an 'examples' folder can be expected.
55     */
56    public function setSourceDirectory(string $directory = '') : void
57    {
58        $this->sourceDirectory = $directory;
59    }
60
61    /**
62     * Returns the project's root directory where an 'examples' folder can be expected.
63     */
64    public function getSourceDirectory() : string
65    {
66        return $this->sourceDirectory;
67    }
68
69    /**
70     * Registers a series of directories that may contain examples.
71     *
72     * @param string[] $directories
73     */
74    public function setExampleDirectories(array $directories) : void
75    {
76        $this->exampleDirectories = $directories;
77    }
78
79    /**
80     * Returns a series of directories that may contain examples.
81     *
82     * @return string[]
83     */
84    public function getExampleDirectories() : array
85    {
86        return $this->exampleDirectories;
87    }
88
89    /**
90     * Attempts to find the requested example file and returns its contents or null if no file was found.
91     *
92     * This method will try several methods in search of the given example file, the first one it encounters is
93     * returned:
94     *
95     * 1. Iterates through all examples folders for the given filename
96     * 2. Checks the source folder for the given filename
97     * 3. Checks the 'examples' folder in the current working directory for examples
98     * 4. Checks the path relative to the current working directory for the given filename
99     *
100     * @return string[] all lines of the example file
101     */
102    private function getExampleFileContents(string $filename) : ?array
103    {
104        $normalizedPath = null;
105
106        foreach ($this->exampleDirectories as $directory) {
107            $exampleFileFromConfig = $this->constructExamplePath($directory, $filename);
108            if (is_readable($exampleFileFromConfig)) {
109                $normalizedPath = $exampleFileFromConfig;
110                break;
111            }
112        }
113
114        if (!$normalizedPath) {
115            if (is_readable($this->getExamplePathFromSource($filename))) {
116                $normalizedPath = $this->getExamplePathFromSource($filename);
117            } elseif (is_readable($this->getExamplePathFromExampleDirectory($filename))) {
118                $normalizedPath = $this->getExamplePathFromExampleDirectory($filename);
119            } elseif (is_readable($filename)) {
120                $normalizedPath = $filename;
121            }
122        }
123
124        $lines = $normalizedPath && is_readable($normalizedPath) ? file($normalizedPath) : false;
125
126        return $lines !== false ? $lines : null;
127    }
128
129    /**
130     * Get example filepath based on the example directory inside your project.
131     */
132    private function getExamplePathFromExampleDirectory(string $file) : string
133    {
134        return getcwd() . DIRECTORY_SEPARATOR . 'examples' . DIRECTORY_SEPARATOR . $file;
135    }
136
137    /**
138     * Returns a path to the example file in the given directory..
139     */
140    private function constructExamplePath(string $directory, string $file) : string
141    {
142        return rtrim($directory, '\\/') . DIRECTORY_SEPARATOR . $file;
143    }
144
145    /**
146     * Get example filepath based on sourcecode.
147     */
148    private function getExamplePathFromSource(string $file) : string
149    {
150        return sprintf(
151            '%s%s%s',
152            trim($this->getSourceDirectory(), '\\/'),
153            DIRECTORY_SEPARATOR,
154            trim($file, '"')
155        );
156    }
157}
158