...

/

Developing a BC Break Scan Class

Developing a BC Break Scan Class

Learn about the infrastructural methods that are needed to create a BC break scan in PHP 8.

The BreakScan class is oriented toward a single file. In this class, we define methods that utilize the various break scan configuration just covered. If we need to scan multiple files, the calling program produces a list of files and passes them to BreakScan one at a time.

The BreakScan class can be broken down into two main parts: methods that define infrastructure and methods that define how to conduct given scans. The latter is primarily dictated by the structure of the configuration file. For each configuration file section, we’ll need a BreakScan class method.

Let’s have a look at the infrastructural methods first.

Defining BreakScan class infrastructural methods

In this section, we’ll have a look at the initial part of the BreakScan class. We also cover methods that perform infrastructure-related activities:

Press + to interact
<?php
declare(strict_types=1);
namespace Php8\Migration;
use InvalidArgumentException;
use UnexpectedValueException;
/**
* Looks for things that might your code after a PHP 8 migration
*
* @todo: add line number of potential break (use file($fn) instead of file_get_contents($fn))
* @author: doug@unlikelysource.com
*/
class BreakScan
{
const ERR_MAGIC_SIGNATURE = 'WARNING: magic method signature for %s does not appear to match required signature';
const ERR_REMOVED = 'WARNING: the following function has been removed: %s. Use this instead: %s';
const ERR_IS_RESOURCE = 'WARNING: this function no longer produces a resource: %s. Usage of "is_resource($item)" should be replaced with "!empty($item)';
const ERR_MISSING_KEY = 'ERROR: missing configuration key %s';
const ERR_INVALID_KEY = 'ERROR: this configuration key is either missing or not callable: ';
const ERR_FILE_NOT_FOUND = 'ERROR: file not found: %s';
const WARN_BC_BREAKS = 'WARNING: the code in this file might not be compatible with PHP 8';
const NO_BC_BREAKS = 'SUCCESS: the code scanned in this file is potentially compatible with PHP 8';
const MAGIC_METHODS = 'The following magic methods were detected:';
const OK_PASSED = 'PASSED this scan: %s';
const TOTAL_BREAKS = 'Total potential BC breaks: %d' . PHP_EOL;
const KEY_REMOVED = 'removed';
const KEY_CALLBACK = 'callbacks';
const KEY_MAGIC = 'magic';
const KEY_RESOURCE = 'resource';
public $config = [];
public $contents = '';
public $messages = [];
public $magic = [];
/**
* @param array $config : scan config
*/
public function __construct(array $config)
{
$this->config = $config;
$required = [self::KEY_CALLBACK, self::KEY_REMOVED, self::KEY_MAGIC, self::KEY_RESOURCE];
foreach ($required as $key) {
if (!isset($this->config[$key])) {
$message = sprintf(self::ERR_MISSING_KEY, $key);
throw new InvalidArgumentException($message);
}
}
}
/**
* Grabs contents
* Initializes messages to []
* Converts "\r" and "\n" to ' '
*
* @param string $fn : name of file to scan
* @return string $name : classname
*/
public function getFileContents(string $fn) : string
{
if (!file_exists($fn)) {
$this->contents = '';
throw new InvalidArgumentException(sprintf(self::ERR_FILE_NOT_FOUND, $fn));
}
$this->clearMessages();
$this->contents = file_get_contents($fn);
$this->contents = str_replace(["\r","\n"],['', ' '], $this->contents);
return $this->contents;
}
/**
* Extracts the value immediately following the supplied word up until the supplied end
*
* @param string $contents : text to search (usually $this->contents)
* @param string $key : starting keyword or set of characters
* @param string $delim : ending delimiter
* @return string $name : classname
*/
public static function getKeyValue(string $contents, string $key, string $delim)
{
$pos = strpos($contents, $key);
if ($pos === FALSE) return '';
$end = strpos($contents, $delim, $pos + strlen($key) + 1);
$key = substr($contents, $pos + strlen($key), $end - $pos - strlen($key));
if (is_string($key)) {
$key = trim($key);
} else {
$key = '';
}
$key = trim($key);
return $key;
}
/**
* Clears messages
*
* @return void
*/
public function clearMessages() : void
{
$this->messages = [];
$this->magic = [];
}
/**
* Returns messages
*
* @param bool $clear : If TRUE, reset messages to []
* @return array $messages : accumulated messages
*/
public function getMessages(bool $clear = FALSE) : array
{
$messages = $this->messages;
if ($clear) $this->clearMessages();
return $messages;
}
/**
* Returns 0 and adds OK message
*
* @param string $function
* @return int 0
*/
public function passedOK(string $function) : int
{
$this->messages[] = sprintf(self::OK_PASSED, $function);
return 0;
}
/**
* Runs all scans
*
* @return int $found : number of potential BC breaks found
*/
public function runAllScans() : int
{
$found = 0;
$found += $this->scanRemovedFunctions();
$found += $this->scanIsResource();
$found += $this->scanMagicSignatures();
echo __METHOD__ . ':' . var_export($this->messages, TRUE) . "\n";
$found += $this->scanFromCallbacks();
return $found;
}
?>

Let’s get into the code.

  • Lines 2–5: First, we set up the class infrastructure, placing it in the current directory.

  • Lines 14–24: Next, we define a set of class constants to render messages indicating the nature of any given post-scan failure.

  • Lines 25–28: We also define a set of constants that represent configuration array keys. We do this to maintain consistency between key definitions in the configuration file and calling program.

  • Lines 30–33: We then initialize key properties representing the configuration, the contents of the file to be scanned, and any messages.

  • Lines 37–47: The __construct() method accepts our break scan configuration file as an argument, and cycles through all of the keys to ensure they exist. ...