*/ public function register() { return Collections::ooCanImplement() + [\T_INTERFACE => \T_INTERFACE]; } /** * Processes this test, when one of its tokens is encountered. * * @since 10.0.0 * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $stackPtr The position of the current token in the * stack passed in $tokens. * * @return void */ public function process(File $phpcsFile, $stackPtr) { if (ScannedCode::shouldRunOnOrAbove('8.1') === false) { return; } $tokens = $phpcsFile->getTokens(); $class = $tokens[$stackPtr]; $this->updateInterfaceList(); if ($class['code'] === \T_INTERFACE) { $extendedInterfaces = ObjectDeclarations::findExtendedInterfaceNames($phpcsFile, $stackPtr); if (empty($extendedInterfaces)) { // Interface doesn't extend other interfaces. return; } $extendedInterfaces = $this->cleanInterfaceNames($extendedInterfaces); $intersection = \array_intersect($extendedInterfaces, $this->interfaceList); if (empty($intersection) === true) { // Interface doesn't extend a PHP native, collected or user provided serializable interface. return; } /* * Okay, so we have an interface which extends Serializable in one way or another. */ if (ScannedCode::shouldRunOnOrBelow('7.3') === false) { /* * Check if the interface requires implementation of the magic methods. */ $hasMagic = $this->findMagicMethods($phpcsFile, $stackPtr); if ($hasMagic['__serialize'] === true && $hasMagic['__unserialize'] === true) { $message = 'When an interface enforces implementation of the magic __serialize() and __unserialize() methods and the code base does not need to support PHP < 7.4, the interface no longer needs to extend the Serializable interface.'; $code = 'RedundantSerializableExtension'; $phpcsFile->addWarning($message, $stackPtr, $code); } } /* * - Check if the interface is already in the list of interfaces to check for. * - If not, we should recommend to the user to add it to the $serializableInterfaces * property and to rescan the codebase. * - And in the mean time, we add the interface to the "Collected" list. */ $interfaceName = ObjectDeclarations::getName($phpcsFile, $stackPtr); $interfaceNameLC = (empty($interfaceName) === false) ? \strtolower($interfaceName) : ''; if (empty($this->customSerializableInterfaces) === false && \in_array($interfaceNameLC, $this->customSerializableInterfaces, true) === true ) { // Interface has already been added to the list of additional interfaces to find. return; } $this->collectedInterfaces[] = $interfaceNameLC; $this->interfaceList[] = $interfaceNameLC; $message = 'The "%1$s" interface extends the serializable %2$s interface%3$s. For the PHPCompatibility.Interface.RemovedSerializable sniff to be reliable, the name of this interface needs to be added to the list of interface implementations to find. Please add the interface name "%1$s" to the "serializableInterfaces" property for this sniff in your custom ruleset and rescan the codebase.'; $code = 'MissingInterface'; $data = [ $interfaceName, \implode(', ', $intersection), (\count($intersection) > 1) ? 's' : '', ]; $phpcsFile->addWarning($message, $stackPtr, $code, $data); // Nothing more to do for interfaces. return; } if ($class['code'] === \T_CLASS) { // Anon classes/enums cannot be abstract. $classProps = ObjectDeclarations::getClassProperties($phpcsFile, $stackPtr); if ($classProps['is_abstract'] === true) { // We cannot determine compliance for abstract classes. return; } } $implementedInterfaces = ObjectDeclarations::findImplementedInterfaceNames($phpcsFile, $stackPtr); if (empty($implementedInterfaces)) { // Class/enum doesn't implement any interface. return; } $implementedInterfaces = $this->cleanInterfaceNames($implementedInterfaces); $matchedInterfaces = \array_intersect($implementedInterfaces, $this->interfaceList); if (empty($matchedInterfaces) === true) { // Class/enum doesn't implement any of the known Serializable interfaces. return; } if ($class['code'] === \T_ENUM) { // Enums cannot declare the magic __serialize() and __unserialize() methods. $message = 'The Serializable interface is deprecated since PHP 8.1.'; $code = 'DeprecatedOnEnum'; $phpcsFile->addWarning($message, $stackPtr, $code); return; } /* * Okay, if we're still here, we know that this is a class which implements * the Serializable interface. */ $hasMagic = $this->findMagicMethods($phpcsFile, $stackPtr); if ($hasMagic['__serialize'] === true && $hasMagic['__unserialize'] === true) { /* * If PHP < 7.4 does not need to be supported, recommend removing the Serializable implementation, * but only for direct implementations of Serializable as other interfaces may provide additional functionality. */ if (ScannedCode::shouldRunOnOrBelow('7.3') === false && \array_intersect($matchedInterfaces, $this->phpSerializableInterfaces) !== [] ) { $message = 'When the magic __serialize() and __unserialize() methods are available and the code base does not need to support PHP < 7.4, the implementation of the Serializable interface can be removed.'; $code = 'RedundantSerializableImplementation'; $phpcsFile->addWarning($message, $stackPtr, $code); } // Both magic methods have been found, we're good. return; } $methods = ''; if ($hasMagic['__serialize'] === false && $hasMagic['__unserialize'] === false) { $methods = '__serialize() and __unserialize()'; } elseif ($hasMagic['__serialize'] === false) { $methods = '__serialize()'; } elseif ($hasMagic['__unserialize'] === false) { $methods = '__unserialize()'; } $message = '"Only Serializable" classes are deprecated since PHP 8.1. The magic __serialize() and __unserialize() methods need to be implemented for cross-version compatibility. Missing implementation of: ' . $methods; $code = 'Deprecated'; $phpcsFile->addWarning($message, $stackPtr, $code); } /** * Update the interface list to match against. * * @since 10.0.0 * * @return void */ private function updateInterfaceList() { if (isset($this->lastSeenSerializableInterfaces) && $this->lastSeenSerializableInterfaces === $this->serializableInterfaces ) { // Nothing to do. return; } // Make sure that all name comparisons against the extra list will be done in a case-insensitive manner. $this->customSerializableInterfaces = $this->serializableInterfaces; if (empty($this->customSerializableInterfaces) === false) { $this->customSerializableInterfaces = $this->cleanInterfaceNames($this->customSerializableInterfaces); } // Overwrite any previously set list to clear out any "extras" no longer set. $interfaceList = \array_merge($this->phpSerializableInterfaces, $this->customSerializableInterfaces, $this->collectedInterfaces); $this->interfaceList = \array_unique($interfaceList); $this->lastSeenSerializableInterfaces = $this->serializableInterfaces; } /** * Lowercase the interface names and remove leading namespace separators. * * @since 10.0.0 * * @param array $interfaceNames List of interface names. * * @return array */ private function cleanInterfaceNames($interfaceNames) { // Make double sure that this is an array. $interfaceNames = (array) $interfaceNames; $interfaceNames = \array_map('strtolower', $interfaceNames); $interfaceNames = \array_map('ltrim', $interfaceNames, \array_fill(0, \count($interfaceNames), '\\')); return $interfaceNames; } /** * Check whether the magic methods have been declared within the class/interface. * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $stackPtr The position of the current token in the * stack passed in $tokens. * * @return array Array with two keys: '__serialize' and '__unserialize'. * The values are boolean indicators of whether the method declarations were found. */ private function findMagicMethods($phpcsFile, $stackPtr) { $ooMethods = ObjectDeclarations::getDeclaredMethods($phpcsFile, $stackPtr); $ooMethodsLC = \array_change_key_case($ooMethods, \CASE_LOWER); return [ '__serialize' => isset($ooMethodsLC['__serialize']), '__unserialize' => isset($ooMethodsLC['__unserialize']), ]; } }