@@ -49,13 +49,7 @@ final class DevToolsPathResolver
4949 */
5050 public static function getPackagePath (string $ path = '' ): string
5151 {
52- $ packageDirectory = \dirname (__DIR__ , 2 );
53-
54- if ('' !== $ path && Path::isAbsolute ($ path )) {
55- throw new InvalidArgumentException ('The DevTools package path MUST be relative to the package root. ' );
56- }
57-
58- return Path::join ($ packageDirectory , $ path );
52+ return self ::resolvePackageRelativePath ($ path );
5953 }
6054
6155 /**
@@ -88,6 +82,28 @@ public static function getResourcesPath(string $path = ''): string
8882 return self ::getPackagePath (Path::join (self ::RESOURCES , $ path ));
8983 }
9084
85+ /**
86+ * Returns a packaged path rendered relative to the active project root when possible.
87+ *
88+ * When the project root and package root do not share a filesystem root,
89+ * the packaged absolute path MUST be returned unchanged so globally
90+ * installed DevTools can still point hooks at the packaged fallback file.
91+ *
92+ * @param string $path the relative path under the package root
93+ * @param string $projectPath an optional project root path; defaults to the working project root
94+ * @param string $packagePath an optional package root path; defaults to the current package root
95+ */
96+ public static function getPackagePathRelativeToProject (
97+ string $ path ,
98+ string $ projectPath = '' ,
99+ string $ packagePath = '' ,
100+ ): string {
101+ return self ::relativizePathFromProject (
102+ self ::resolvePackageRelativePath ($ path , $ packagePath ),
103+ self ::resolveProjectPath ($ projectPath ),
104+ );
105+ }
106+
91107 /**
92108 * Returns the active Composer autoload file for the current DevTools installation mode.
93109 *
@@ -99,13 +115,7 @@ public static function getResourcesPath(string $path = ''): string
99115 */
100116 public static function getRuntimeAutoloadPath (string $ packagePath = '' ): string
101117 {
102- $ packagePath = Path::canonicalize ('' === $ packagePath ? self ::getPackagePath () : $ packagePath );
103-
104- if (self ::isInstalledAsDependency ($ packagePath )) {
105- return Path::canonicalize (Path::join ($ packagePath , '.. ' , '.. ' , 'autoload.php ' ));
106- }
107-
108- return Path::join ($ packagePath , 'vendor ' , 'autoload.php ' );
118+ return Path::join (self ::getRuntimeVendorRoot ($ packagePath ), 'autoload.php ' );
109119 }
110120
111121 /**
@@ -119,13 +129,7 @@ public static function getRuntimeAutoloadPath(string $packagePath = ''): string
119129 */
120130 public static function getRuntimeToolBinaryPath (string $ binary , string $ packagePath = '' ): string
121131 {
122- $ packagePath = Path::canonicalize ('' === $ packagePath ? self ::getPackagePath () : $ packagePath );
123-
124- if (self ::isInstalledAsDependency ($ packagePath )) {
125- return Path::canonicalize (Path::join ($ packagePath , '.. ' , '.. ' , 'bin ' , $ binary ));
126- }
127-
128- return Path::join ($ packagePath , 'vendor ' , 'bin ' , $ binary );
132+ return self ::getRuntimeVendorPath (Path::join ('bin ' , $ binary ), $ packagePath );
129133 }
130134
131135 /**
@@ -139,14 +143,7 @@ public static function getRuntimeToolBinaryPath(string $binary, string $packageP
139143 */
140144 public static function getRuntimeVendorPath (string $ path , string $ packagePath = '' ): string
141145 {
142- $ packagePath = Path::canonicalize ('' === $ packagePath ? self ::getPackagePath () : $ packagePath );
143- $ vendorPath = self ::normalizeVendorRelativePath ($ path );
144-
145- if (self ::isInstalledAsDependency ($ packagePath )) {
146- return Path::canonicalize (Path::join ($ packagePath , '.. ' , '.. ' , $ vendorPath ));
147- }
148-
149- return Path::join ($ packagePath , 'vendor ' , $ vendorPath );
146+ return Path::join (self ::getRuntimeVendorRoot ($ packagePath ), self ::normalizeVendorRelativePath ($ path ));
150147 }
151148
152149 /**
@@ -165,14 +162,10 @@ public static function getPreferredToolBinaryPath(
165162 string $ projectPath = '' ,
166163 string $ packagePath = '' ,
167164 ): string {
168- $ projectPath = '' === $ projectPath ? WorkingProjectPathResolver::getProjectPath () : $ projectPath ;
169- $ projectBinaryPath = Path::join ($ projectPath , 'vendor ' , 'bin ' , $ binary );
170-
171- if (file_exists ($ projectBinaryPath )) {
172- return $ projectBinaryPath ;
173- }
174-
175- return self ::getRuntimeToolBinaryPath ($ binary , $ packagePath );
165+ return self ::preferExistingPath (
166+ self ::getProjectVendorPath (Path::join ('bin ' , $ binary ), $ projectPath ),
167+ self ::getRuntimeToolBinaryPath ($ binary , $ packagePath ),
168+ );
176169 }
177170
178171 /**
@@ -191,15 +184,10 @@ public static function getPreferredVendorPath(
191184 string $ projectPath = '' ,
192185 string $ packagePath = '' ,
193186 ): string {
194- $ projectPath = '' === $ projectPath ? WorkingProjectPathResolver::getProjectPath () : $ projectPath ;
195- $ vendorPath = self ::normalizeVendorRelativePath ($ path );
196- $ projectVendorPath = Path::join ($ projectPath , 'vendor ' , $ vendorPath );
197-
198- if (file_exists ($ projectVendorPath )) {
199- return $ projectVendorPath ;
200- }
201-
202- return self ::getRuntimeVendorPath ($ vendorPath , $ packagePath );
187+ return self ::preferExistingPath (
188+ self ::getProjectVendorPath ($ path , $ projectPath ),
189+ self ::getRuntimeVendorPath ($ path , $ packagePath ),
190+ );
203191 }
204192
205193 /**
@@ -209,9 +197,7 @@ public static function getPreferredVendorPath(
209197 */
210198 public static function isInstalledAsDependency (string $ packagePath = '' ): bool
211199 {
212- $ packagePath = Path::canonicalize ('' === $ packagePath ? self ::getPackagePath () : $ packagePath );
213-
214- return str_contains ($ packagePath , self ::VENDOR_PACKAGE_PATH );
200+ return str_contains (self ::resolvePackageRoot ($ packagePath ), self ::VENDOR_PACKAGE_PATH );
215201 }
216202
217203 /**
@@ -239,4 +225,109 @@ private static function normalizeVendorRelativePath(string $path): string
239225
240226 return $ path ;
241227 }
228+
229+ /**
230+ * Ensures packaged paths stay relative to the DevTools package root.
231+ *
232+ * @param string $path the package-relative path to validate
233+ */
234+ private static function assertRelativePackagePath (string $ path ): void
235+ {
236+ if ('' !== $ path && Path::isAbsolute ($ path )) {
237+ throw new InvalidArgumentException ('The DevTools package path MUST be relative to the package root. ' );
238+ }
239+ }
240+
241+ /**
242+ * Returns a canonical path under the DevTools package root.
243+ *
244+ * @param string $path the package-relative path to resolve
245+ * @param string $packagePath an optional package root path; defaults to the current package root
246+ */
247+ private static function resolvePackageRelativePath (string $ path = '' , string $ packagePath = '' ): string
248+ {
249+ self ::assertRelativePackagePath ($ path );
250+
251+ return Path::canonicalize (Path::join (self ::resolvePackageRoot ($ packagePath ), $ path ));
252+ }
253+
254+ /**
255+ * Returns the canonical DevTools package root.
256+ *
257+ * @param string $packagePath an optional package root path; defaults to the current package root
258+ */
259+ private static function resolvePackageRoot (string $ packagePath = '' ): string
260+ {
261+ return Path::canonicalize ('' === $ packagePath ? \dirname (__DIR__ , 2 ) : $ packagePath );
262+ }
263+
264+ /**
265+ * Returns the canonical working project root.
266+ *
267+ * @param string $projectPath an optional project root path; defaults to the working project root
268+ */
269+ private static function resolveProjectPath (string $ projectPath = '' ): string
270+ {
271+ return Path::canonicalize (WorkingProjectPathResolver::getProjectPath ($ projectPath ));
272+ }
273+
274+ /**
275+ * Returns the active Composer vendor root for the current DevTools installation mode.
276+ *
277+ * @param string $packagePath an optional package root path; defaults to the current package root
278+ */
279+ private static function getRuntimeVendorRoot (string $ packagePath = '' ): string
280+ {
281+ $ packagePath = self ::resolvePackageRoot ($ packagePath );
282+
283+ if (self ::isInstalledAsDependency ($ packagePath )) {
284+ return Path::canonicalize (Path::join ($ packagePath , '.. ' , '.. ' ));
285+ }
286+
287+ return Path::join ($ packagePath , 'vendor ' );
288+ }
289+
290+ /**
291+ * Returns a vendor path under the active project root.
292+ *
293+ * @param string $path the vendor-relative path to resolve
294+ * @param string $projectPath an optional project root path; defaults to the working project root
295+ */
296+ private static function getProjectVendorPath (string $ path , string $ projectPath = '' ): string
297+ {
298+ return Path::join (self ::resolveProjectPath ($ projectPath ), 'vendor ' , self ::normalizeVendorRelativePath ($ path ));
299+ }
300+
301+ /**
302+ * Returns the preferred path when a project-local candidate exists.
303+ *
304+ * @param string $preferredPath the project-local candidate path
305+ * @param string $fallbackPath the runtime fallback path
306+ */
307+ private static function preferExistingPath (string $ preferredPath , string $ fallbackPath ): string
308+ {
309+ if (file_exists ($ preferredPath )) {
310+ return $ preferredPath ;
311+ }
312+
313+ return $ fallbackPath ;
314+ }
315+
316+ /**
317+ * Returns a path relative to the project root when possible.
318+ *
319+ * When paths do not share the same filesystem root, the original absolute
320+ * path MUST be returned unchanged so callers still receive a usable path.
321+ *
322+ * @param string $path the absolute path to relativize
323+ * @param string $projectPath the absolute project root used as base path
324+ */
325+ private static function relativizePathFromProject (string $ path , string $ projectPath ): string
326+ {
327+ try {
328+ return Path::makeRelative ($ path , $ projectPath );
329+ } catch (InvalidArgumentException ) {
330+ return $ path ;
331+ }
332+ }
242333}
0 commit comments