Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F1429645
Plugin.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
19 KB
Referenced Files
None
Subscribers
None
Plugin.php
View Options
<?php
/**
* This file is part of the Dealerdirect PHP_CodeSniffer Standards
* Composer Installer Plugin package.
*
* @copyright 2016-2022 Dealerdirect B.V.
* @license MIT
*/
namespace
PHPCSStandards\Composer\Plugin\Installers\PHPCodeSniffer
;
use
Composer\Composer
;
use
Composer\EventDispatcher\EventSubscriberInterface
;
use
Composer\IO\IOInterface
;
use
Composer\Package\AliasPackage
;
use
Composer\Package\PackageInterface
;
use
Composer\Package\RootPackageInterface
;
use
Composer\Plugin\PluginInterface
;
use
Composer\Script\Event
;
use
Composer\Script\ScriptEvents
;
use
Composer\Util\Filesystem
;
use
Composer\Util\ProcessExecutor
;
use
Symfony\Component\Finder\Finder
;
use
Symfony\Component\Process\Exception\LogicException
;
use
Symfony\Component\Process\Exception\ProcessFailedException
;
use
Symfony\Component\Process\Exception\RuntimeException
;
use
Symfony\Component\Process\PhpExecutableFinder
;
/**
* PHP_CodeSniffer standard installation manager.
*
* @author Franck Nijhof <franck.nijhof@dealerdirect.com>
*/
class
Plugin
implements
PluginInterface
,
EventSubscriberInterface
{
const
KEY_MAX_DEPTH
=
'phpcodesniffer-search-depth'
;
const
MESSAGE_ERROR_WRONG_MAX_DEPTH
=
'The value of "%s" (in the composer.json "extra".section) must be an integer larger than %d, %s given.'
;
const
MESSAGE_NOT_INSTALLED
=
'PHPCodeSniffer is not installed'
;
const
MESSAGE_NOTHING_TO_INSTALL
=
'No PHPCS standards to install or update'
;
const
MESSAGE_PLUGIN_UNINSTALLED
=
'PHPCodeSniffer Composer Installer is uninstalled'
;
const
MESSAGE_RUNNING_INSTALLER
=
'Running PHPCodeSniffer Composer Installer'
;
const
PACKAGE_NAME
=
'squizlabs/php_codesniffer'
;
const
PACKAGE_TYPE
=
'phpcodesniffer-standard'
;
const
PHPCS_CONFIG_REGEX
=
'`%s:[^
\r\n
]+`'
;
const
PHPCS_CONFIG_KEY
=
'installed_paths'
;
const
PLUGIN_NAME
=
'dealerdirect/phpcodesniffer-composer-installer'
;
/**
* @var Composer
*/
private
$composer
;
/**
* @var string
*/
private
$cwd
;
/**
* @var Filesystem
*/
private
$filesystem
;
/**
* @var array
*/
private
$installedPaths
;
/**
* @var IOInterface
*/
private
$io
;
/**
* @var ProcessExecutor
*/
private
$processExecutor
;
/**
* Triggers the plugin's main functionality.
*
* Makes it possible to run the plugin as a custom command.
*
* @param Event $event
*
* @throws \InvalidArgumentException
* @throws \RuntimeException
* @throws LogicException
* @throws ProcessFailedException
* @throws RuntimeException
*/
public
static
function
run
(
Event
$event
)
{
$io
=
$event
->
getIO
();
$composer
=
$event
->
getComposer
();
$instance
=
new
static
();
$instance
->
io
=
$io
;
$instance
->
composer
=
$composer
;
$instance
->
init
();
$instance
->
onDependenciesChangedEvent
();
}
/**
* {@inheritDoc}
*
* @throws \RuntimeException
* @throws LogicException
* @throws ProcessFailedException
* @throws RuntimeException
*/
public
function
activate
(
Composer
$composer
,
IOInterface
$io
)
{
$this
->
composer
=
$composer
;
$this
->
io
=
$io
;
$this
->
init
();
}
/**
* {@inheritDoc}
*/
public
function
deactivate
(
Composer
$composer
,
IOInterface
$io
)
{
}
/**
* {@inheritDoc}
*/
public
function
uninstall
(
Composer
$composer
,
IOInterface
$io
)
{
}
/**
* Prepares the plugin so it's main functionality can be run.
*
* @throws \RuntimeException
* @throws LogicException
* @throws ProcessFailedException
* @throws RuntimeException
*/
private
function
init
()
{
$this
->
cwd
=
getcwd
();
$this
->
installedPaths
=
array
();
$this
->
processExecutor
=
new
ProcessExecutor
(
$this
->
io
);
$this
->
filesystem
=
new
Filesystem
(
$this
->
processExecutor
);
}
/**
* {@inheritDoc}
*/
public
static
function
getSubscribedEvents
()
{
return
array
(
ScriptEvents
::
POST_INSTALL_CMD
=>
array
(
array
(
'onDependenciesChangedEvent'
,
0
),
),
ScriptEvents
::
POST_UPDATE_CMD
=>
array
(
array
(
'onDependenciesChangedEvent'
,
0
),
),
);
}
/**
* Entry point for post install and post update events.
*
* @throws \InvalidArgumentException
* @throws LogicException
* @throws ProcessFailedException
* @throws RuntimeException
*/
public
function
onDependenciesChangedEvent
()
{
$io
=
$this
->
io
;
$isVerbose
=
$io
->
isVerbose
();
$exitCode
=
0
;
if
(
$isVerbose
)
{
$io
->
write
(
sprintf
(
'<info>%s</info>'
,
self
::
MESSAGE_RUNNING_INSTALLER
));
}
if
(
$this
->
isPHPCodeSnifferInstalled
()
===
true
)
{
$this
->
loadInstalledPaths
();
$installPathCleaned
=
$this
->
cleanInstalledPaths
();
$installPathUpdated
=
$this
->
updateInstalledPaths
();
if
(
$installPathCleaned
===
true
||
$installPathUpdated
===
true
)
{
$exitCode
=
$this
->
saveInstalledPaths
();
}
elseif
(
$isVerbose
)
{
$io
->
write
(
sprintf
(
'<info>%s</info>'
,
self
::
MESSAGE_NOTHING_TO_INSTALL
));
}
}
else
{
$pluginPackage
=
$this
->
composer
->
getRepositoryManager
()
->
getLocalRepository
()
->
findPackages
(
self
::
PLUGIN_NAME
)
;
$isPluginUninstalled
=
count
(
$pluginPackage
)
===
0
;
if
(
$isPluginUninstalled
)
{
if
(
$isVerbose
)
{
$io
->
write
(
sprintf
(
'<info>%s</info>'
,
self
::
MESSAGE_PLUGIN_UNINSTALLED
));
}
}
else
{
$exitCode
=
1
;
if
(
$isVerbose
)
{
$io
->
write
(
sprintf
(
'<error>%s</error>'
,
self
::
MESSAGE_NOT_INSTALLED
));
}
}
}
return
$exitCode
;
}
/**
* Load all paths from PHP_CodeSniffer into an array.
*
* @throws LogicException
* @throws ProcessFailedException
* @throws RuntimeException
*/
private
function
loadInstalledPaths
()
{
if
(
$this
->
isPHPCodeSnifferInstalled
()
===
true
)
{
$this
->
processExecutor
->
execute
(
$this
->
getPhpcsCommand
()
.
' --config-show'
,
$output
,
$this
->
getPHPCodeSnifferInstallPath
()
);
$regex
=
sprintf
(
self
::
PHPCS_CONFIG_REGEX
,
self
::
PHPCS_CONFIG_KEY
);
if
(
preg_match
(
$regex
,
$output
,
$match
)
===
1
)
{
$phpcsInstalledPaths
=
str_replace
(
self
::
PHPCS_CONFIG_KEY
.
': '
,
''
,
$match
[
0
]);
$phpcsInstalledPaths
=
trim
(
$phpcsInstalledPaths
);
if
(
$phpcsInstalledPaths
!==
''
)
{
$this
->
installedPaths
=
explode
(
','
,
$phpcsInstalledPaths
);
}
}
}
}
/**
* Save all coding standard paths back into PHP_CodeSniffer
*
* @throws LogicException
* @throws ProcessFailedException
* @throws RuntimeException
*
* @return int Exit code. 0 for success, 1 or higher for failure.
*/
private
function
saveInstalledPaths
()
{
// Check if we found installed paths to set.
if
(
count
(
$this
->
installedPaths
)
!==
0
)
{
sort
(
$this
->
installedPaths
);
$paths
=
implode
(
','
,
$this
->
installedPaths
);
$arguments
=
array
(
'--config-set'
,
self
::
PHPCS_CONFIG_KEY
,
$paths
);
$configMessage
=
sprintf
(
'PHP CodeSniffer Config <info>%s</info> <comment>set to</comment> <info>%s</info>'
,
self
::
PHPCS_CONFIG_KEY
,
$paths
);
}
else
{
// Delete the installed paths if none were found.
$arguments
=
array
(
'--config-delete'
,
self
::
PHPCS_CONFIG_KEY
);
$configMessage
=
sprintf
(
'PHP CodeSniffer Config <info>%s</info> <comment>delete</comment>'
,
self
::
PHPCS_CONFIG_KEY
);
}
// Prepare message in case of failure
$failMessage
=
sprintf
(
'Failed to set PHP CodeSniffer <info>%s</info> Config'
,
self
::
PHPCS_CONFIG_KEY
);
// Okay, lets rock!
$command
=
vsprintf
(
'%s %s'
,
array
(
'phpcs command'
=>
$this
->
getPhpcsCommand
(),
'arguments'
=>
implode
(
' '
,
$arguments
),
)
);
$exitCode
=
$this
->
processExecutor
->
execute
(
$command
,
$configResult
,
$this
->
getPHPCodeSnifferInstallPath
());
if
(
$exitCode
===
0
)
{
$exitCode
=
$this
->
verifySaveSuccess
();
}
if
(
$exitCode
===
0
)
{
$this
->
io
->
write
(
$configMessage
);
}
else
{
$this
->
io
->
write
(
$failMessage
);
}
if
(
$this
->
io
->
isVerbose
()
&&
!
empty
(
$configResult
))
{
$this
->
io
->
write
(
sprintf
(
'<info>%s</info>'
,
$configResult
));
}
return
$exitCode
;
}
/**
* Verify that the paths which were expected to be saved, have been.
*
* @return int Exit code. 0 for success, 1 for failure.
*/
private
function
verifySaveSuccess
()
{
$exitCode
=
1
;
$expectedPaths
=
$this
->
installedPaths
;
// Request the currently set installed paths after the save.
$this
->
loadInstalledPaths
();
$registeredPaths
=
array_intersect
(
$this
->
installedPaths
,
$expectedPaths
);
$registeredCount
=
count
(
$registeredPaths
);
$expectedCount
=
count
(
$expectedPaths
);
if
(
$expectedCount
===
$registeredCount
)
{
$exitCode
=
0
;
}
if
(
$exitCode
===
1
&&
$this
->
io
->
isVerbose
())
{
$verificationMessage
=
sprintf
(
"Paths to external standards found by the plugin: <info>%s</info>
\n
"
.
'Actual paths registered with PHPCS: <info>%s</info>'
,
implode
(
', '
,
$expectedPaths
),
implode
(
', '
,
$this
->
installedPaths
)
);
$this
->
io
->
write
(
$verificationMessage
);
}
return
$exitCode
;
}
/**
* Get the command to call PHPCS.
*/
protected
function
getPhpcsCommand
()
{
// Determine the path to the main PHPCS file.
$phpcsPath
=
$this
->
getPHPCodeSnifferInstallPath
();
if
(
file_exists
(
$phpcsPath
.
'/bin/phpcs'
)
===
true
)
{
// PHPCS 3.x.
$phpcsExecutable
=
'./bin/phpcs'
;
}
else
{
// PHPCS 2.x.
$phpcsExecutable
=
'./scripts/phpcs'
;
}
return
vsprintf
(
'%s %s'
,
array
(
'php executable'
=>
$this
->
getPhpExecCommand
(),
'phpcs executable'
=>
$phpcsExecutable
,
)
);
}
/**
* Get the path to the current PHP version being used.
*
* Duplicate of the same in the EventDispatcher class in Composer itself.
*/
protected
function
getPhpExecCommand
()
{
$finder
=
new
PhpExecutableFinder
();
$phpPath
=
$finder
->
find
(
false
);
if
(
$phpPath
===
false
)
{
throw
new
\RuntimeException
(
'Failed to locate PHP binary to execute '
.
$phpPath
);
}
$phpArgs
=
$finder
->
findArguments
();
$phpArgs
=
$phpArgs
?
' '
.
implode
(
' '
,
$phpArgs
)
:
''
;
$command
=
ProcessExecutor
::
escape
(
$phpPath
)
.
$phpArgs
.
' -d allow_url_fopen='
.
ProcessExecutor
::
escape
(
ini_get
(
'allow_url_fopen'
))
.
' -d disable_functions='
.
ProcessExecutor
::
escape
(
ini_get
(
'disable_functions'
))
.
' -d memory_limit='
.
ProcessExecutor
::
escape
(
ini_get
(
'memory_limit'
))
;
return
$command
;
}
/**
* Iterate trough all known paths and check if they are still valid.
*
* If path does not exists, is not an directory or isn't readable, the path
* is removed from the list.
*
* @return bool True if changes where made, false otherwise
*/
private
function
cleanInstalledPaths
()
{
$changes
=
false
;
foreach
(
$this
->
installedPaths
as
$key
=>
$path
)
{
// This might be a relative path as well
$alternativePath
=
realpath
(
$this
->
getPHPCodeSnifferInstallPath
()
.
\DIRECTORY_SEPARATOR
.
$path
);
if
(
(
is_dir
(
$path
)
===
false
||
is_readable
(
$path
)
===
false
)
&&
(
$alternativePath
===
false
||
is_dir
(
$alternativePath
)
===
false
||
is_readable
(
$alternativePath
)
===
false
)
)
{
unset
(
$this
->
installedPaths
[
$key
]);
$changes
=
true
;
}
}
return
$changes
;
}
/**
* Check all installed packages (including the root package) against
* the installed paths from PHP_CodeSniffer and add the missing ones.
*
* @return bool True if changes where made, false otherwise
*
* @throws \InvalidArgumentException
* @throws \RuntimeException
*/
private
function
updateInstalledPaths
()
{
$changes
=
false
;
$searchPaths
=
array
();
// Add root package only if it has the expected package type.
if
(
$this
->
composer
->
getPackage
()
instanceof
RootPackageInterface
&&
$this
->
composer
->
getPackage
()->
getType
()
===
self
::
PACKAGE_TYPE
)
{
$searchPaths
[]
=
$this
->
cwd
;
}
$codingStandardPackages
=
$this
->
getPHPCodingStandardPackages
();
foreach
(
$codingStandardPackages
as
$package
)
{
$installPath
=
$this
->
composer
->
getInstallationManager
()->
getInstallPath
(
$package
);
if
(
$this
->
filesystem
->
isAbsolutePath
(
$installPath
)
===
false
)
{
$installPath
=
$this
->
filesystem
->
normalizePath
(
$this
->
cwd
.
\DIRECTORY_SEPARATOR
.
$installPath
);
}
$searchPaths
[]
=
$installPath
;
}
// Nothing to do.
if
(
$searchPaths
===
array
())
{
return
false
;
}
$finder
=
new
Finder
();
$finder
->
files
()
->
depth
(
'<= '
.
$this
->
getMaxDepth
())
->
depth
(
'>= '
.
$this
->
getMinDepth
())
->
ignoreUnreadableDirs
()
->
ignoreVCS
(
true
)
->
in
(
$searchPaths
)
->
name
(
'ruleset.xml'
);
// Process each found possible ruleset.
foreach
(
$finder
as
$ruleset
)
{
$standardsPath
=
$ruleset
->
getPath
();
// Pick the directory above the directory containing the standard, unless this is the project root.
if
(
$standardsPath
!==
$this
->
cwd
)
{
$standardsPath
=
dirname
(
$standardsPath
);
}
// Use relative paths for local project repositories.
if
(
$this
->
isRunningGlobally
()
===
false
)
{
$standardsPath
=
$this
->
filesystem
->
findShortestPath
(
$this
->
getPHPCodeSnifferInstallPath
(),
$standardsPath
,
true
);
}
// De-duplicate and add when directory is not configured.
if
(
in_array
(
$standardsPath
,
$this
->
installedPaths
,
true
)
===
false
)
{
$this
->
installedPaths
[]
=
$standardsPath
;
$changes
=
true
;
}
}
return
$changes
;
}
/**
* Iterates through Composers' local repository looking for valid Coding
* Standard packages.
*
* @return array Composer packages containing coding standard(s)
*/
private
function
getPHPCodingStandardPackages
()
{
$codingStandardPackages
=
array_filter
(
$this
->
composer
->
getRepositoryManager
()->
getLocalRepository
()->
getPackages
(),
function
(
PackageInterface
$package
)
{
if
(
$package
instanceof
AliasPackage
)
{
return
false
;
}
return
$package
->
getType
()
===
Plugin
::
PACKAGE_TYPE
;
}
);
return
$codingStandardPackages
;
}
/**
* Searches for the installed PHP_CodeSniffer Composer package
*
* @param null|string|\Composer\Semver\Constraint\ConstraintInterface $versionConstraint to match against
*
* @return PackageInterface|null
*/
private
function
getPHPCodeSnifferPackage
(
$versionConstraint
=
null
)
{
$packages
=
$this
->
composer
->
getRepositoryManager
()
->
getLocalRepository
()
->
findPackages
(
self
::
PACKAGE_NAME
,
$versionConstraint
);
return
array_shift
(
$packages
);
}
/**
* Returns the path to the PHP_CodeSniffer package installation location
*
* @return string
*/
private
function
getPHPCodeSnifferInstallPath
()
{
return
$this
->
composer
->
getInstallationManager
()->
getInstallPath
(
$this
->
getPHPCodeSnifferPackage
());
}
/**
* Simple check if PHP_CodeSniffer is installed.
*
* @param null|string|\Composer\Semver\Constraint\ConstraintInterface $versionConstraint to match against
*
* @return bool Whether PHP_CodeSniffer is installed
*/
private
function
isPHPCodeSnifferInstalled
(
$versionConstraint
=
null
)
{
return
(
$this
->
getPHPCodeSnifferPackage
(
$versionConstraint
)
!==
null
);
}
/**
* Test if composer is running "global"
* This check kinda dirty, but it is the "Composer Way"
*
* @return bool Whether Composer is running "globally"
*
* @throws \RuntimeException
*/
private
function
isRunningGlobally
()
{
return
(
$this
->
composer
->
getConfig
()->
get
(
'home'
)
===
$this
->
cwd
);
}
/**
* Determines the maximum search depth when searching for Coding Standards.
*
* @return int
*
* @throws \InvalidArgumentException
*/
private
function
getMaxDepth
()
{
$maxDepth
=
3
;
$extra
=
$this
->
composer
->
getPackage
()->
getExtra
();
if
(
array_key_exists
(
self
::
KEY_MAX_DEPTH
,
$extra
))
{
$maxDepth
=
$extra
[
self
::
KEY_MAX_DEPTH
];
$minDepth
=
$this
->
getMinDepth
();
if
(
(
string
)
(
int
)
$maxDepth
!==
(
string
)
$maxDepth
/* Must be an integer or cleanly castable to one */
||
$maxDepth
<=
$minDepth
/* Larger than the minimum */
||
is_float
(
$maxDepth
)
===
true
/* Within the boundaries of integer */
)
{
$message
=
vsprintf
(
self
::
MESSAGE_ERROR_WRONG_MAX_DEPTH
,
array
(
'key'
=>
self
::
KEY_MAX_DEPTH
,
'min'
=>
$minDepth
,
'given'
=>
var_export
(
$maxDepth
,
true
),
)
);
throw
new
\InvalidArgumentException
(
$message
);
}
}
return
(
int
)
$maxDepth
;
}
/**
* Returns the minimal search depth for Coding Standard packages.
*
* Usually this is 0, unless PHP_CodeSniffer >= 3 is used.
*
* @return int
*/
private
function
getMinDepth
()
{
if
(
$this
->
isPHPCodeSnifferInstalled
(
'>= 3.0.0'
)
!==
true
)
{
return
1
;
}
return
0
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, May 16, 17:51 (9 h, 42 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
35/65/b8776bc2023294d299720f1afc65
Default Alt Text
Plugin.php (19 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment