If you’ve ever done OOP in PHP you are fully aware of the __construct method and the role it plays in setting up class properties, among other tasks. But have you ever given any thought on using the opposite of this function: the __destruct method? Were you aware that such a function existed? Keep reading to find about some use cases I’ve personally implemented in the past.
Save Errors to File
Over the years, I’ve had to create various import scripts. For example, grabbing data from a CSV file and landing it onto a database table. Sometimes what would happen is that my script would stop running because it ran into a row with too many values or a value which couldn’t be inserted properly. These things would hinder my progress because I would then go through a process of elimination to see what the troublesome values were and writing conditions to handle them. It was later that I realized I could come up with a solution whereby the script would continue running and be allowed to finish, while catching and logging any issues that arose along the way. The __destruct method was pivotal in achieving this, see the gist below:
| <?php | |
| namespace App\Classes; | |
| use Exception; | |
| use PDO; | |
| use PDOException; | |
| abstract class ParentImportScript | |
| { | |
| /** | |
| * @var PDO $pdo | |
| */ | |
| protected $pdo; | |
| /** | |
| * @var array $errorBag | |
| */ | |
| protected $errorBag; | |
| /** | |
| * @var string $errorFileLocation | |
| */ | |
| protected $errorFileLocation = '/var/www/html/storage/'; | |
| /** | |
| * @var string $startTime | |
| */ | |
| protected $startTime; | |
| /** | |
| * ChildImportScript constructor. | |
| * @param $dsn | |
| * @param $username | |
| * @param $password | |
| * @param $options | |
| */ | |
| public function __construct($dsn, $username, $password, $options = []) | |
| { | |
| $this->pdo = new PDO($dsn, $username, $password, $options); | |
| /* | |
| * This tells PDO to emit a PDOException should there be | |
| * any issues with the query statement it executes. | |
| */ | |
| $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); | |
| /* | |
| * Making sure to initialize the variable | |
| * which holds all the errors. | |
| */ | |
| $this->errorBag = []; | |
| $this->recordStartTime(); | |
| /* | |
| * The responsibility of actually running the import is handled | |
| * by the parent. The child just specifies what data should | |
| * be imported. The possibility to alter in what specific | |
| * manner the import should be performed is present | |
| * however. | |
| */ | |
| $this->handleImport($this->getImportData()); | |
| /* | |
| * Lazily simulating the import taking some arbitrary time. | |
| */ | |
| sleep(10); | |
| } | |
| /** | |
| * @return string | |
| */ | |
| abstract protected function getTable(): string; | |
| /** | |
| * @return array | |
| */ | |
| abstract protected function getImportData(); | |
| /** | |
| * @param array $data | |
| * @return void | |
| */ | |
| protected function handleImport(array $data) | |
| { | |
| foreach ($data as $datum) { | |
| $columnNames = array_keys($datum); | |
| $preparedColumnNames = implode(',', $columnNames); | |
| $preparedValuePlaceholders = implode(',', array_map(function ($column) { | |
| return ":$column"; | |
| }, $columnNames)); | |
| $sql = "INSERT INTO {$this->getTable()} ($preparedColumnNames) VALUES ($preparedValuePlaceholders)"; | |
| try { | |
| $this->pdo->prepare($sql)->execute($datum); | |
| } catch (PDOException $e) { | |
| $this->addExceptionToErrorBag($e); | |
| } | |
| } | |
| } | |
| /** | |
| * @param Exception $e | |
| */ | |
| protected function addExceptionToErrorBag(Exception $e) | |
| { | |
| /* | |
| * The actual data that is recorded can be | |
| * changed to fit your needs. Either | |
| * here or in the child class. | |
| */ | |
| $this->errorBag[] = [ | |
| 'Exception' => get_class($e), | |
| 'Message' => "{$e->getMessage()} {$e->getLine()}", | |
| 'Trace' => $e->getTraceAsString(), | |
| ]; | |
| } | |
| private function getRunningScriptName() | |
| { | |
| $classNamespacePath = explode('\\', get_class($this)); | |
| return array_pop($classNamespacePath); | |
| } | |
| private function recordStartTime() | |
| { | |
| $this->startTime = date('Y-m-d H:i:s'); | |
| $sql = "INSERT INTO scripts_table (script_name, started_at) | |
| VALUES ('{$this->getRunningScriptName()}', '{$this->startTime}')"; | |
| $this->pdo->prepare($sql)->execute(); | |
| } | |
| public function __destruct() | |
| { | |
| if (count($this->errorBag) > 0) { | |
| $className = $this->getRunningScriptName(); | |
| $timestamp = date('Ymd_His'); | |
| $filename = "{$className}_errors_$timestamp.json"; | |
| file_put_contents("$this->errorFileLocation/$filename", json_encode($this->errorBag)); | |
| } | |
| try { | |
| $sql = "UPDATE scripts_table SET completed_at = CURRENT_TIMESTAMP | |
| WHERE script_name = '{$this->getRunningScriptName()}' | |
| AND started_at = '{$this->startTime}';"; | |
| $this->pdo->prepare($sql)->execute(); | |
| } catch (PDOException $e) { | |
| $this->addExceptionToErrorBag($e); | |
| } | |
| } | |
| } |
| <?php | |
| namespace App\Classes; | |
| class ChildImportScript extends ParentImportScript | |
| { | |
| /** | |
| * @return string | |
| */ | |
| protected function getTable(): string | |
| { | |
| return 'some_table'; | |
| } | |
| /** | |
| * @return array | |
| */ | |
| protected function getImportData(): array | |
| { | |
| /* | |
| * This function is meant to illustrate that the actual data we need to be imported | |
| * can be implemented in various different ways. Perhaps at this point you | |
| * need to look for a local CSV file, or you need to run some database | |
| * queries and transform that data in some way and then import it. | |
| * | |
| * For now, I am purposely returning data which I know cannot | |
| * be inserted into a table to trigger a PDOException. | |
| */ | |
| return [ | |
| [ | |
| 'Testing' => true, | |
| ], | |
| ]; | |
| } | |
| } |

ChildImportScript class I created.This resulted in the following file:

For illustration, I’ve gone ahead and created the necessary table in my database. I’ve also added a few more “records” to be inserted, without the correct column names however.
CREATE TABLE `some_table` (
`a_column` VARCHAR(50) NULL DEFAULT NULL,
`created_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP()
) COLLATE='utf8mb4_0900_ai_ci';

Executing the import script again results in another error file, but notice that it didn’t prevent the single valid record from being inserted.


Now we have an easy way to implement more import scripts and be confident that any valid values will be inserted, while providing a nice list of erroneous records to follow up on.
Clean Up Environment After Job Completion
In my previous example I mentioned how I often find myself having to import various CSV files from time to time. In my case, those CSV files sit on a different server where I must download the file locally first before trying to import its contents. The issue then arose of how to ensure that the server environment didn’t unnecessarily house a laundry list of CSV files that only need to be used once. My solution was to discard the file after I was done processing it.
| <?php | |
| namespace App\Classes; | |
| class CsvFileHelper | |
| { | |
| /** | |
| * @var resource $file | |
| */ | |
| protected $file; | |
| /** | |
| * @var string $filePath | |
| */ | |
| protected $filePath; | |
| /** | |
| * @var string $fileName | |
| */ | |
| protected $fileName; | |
| /** | |
| * @var float|int $fileSize File size in KB | |
| */ | |
| protected $fileSize; | |
| /** | |
| * @var string $storagePath | |
| */ | |
| protected $storagePath = '/var/www/html/storage'; | |
| /** | |
| * @var $ssh Ssh2SftpHelper | |
| */ | |
| protected $ssh; | |
| public function __construct($filePath) | |
| { | |
| $this->filePath = $filePath; | |
| $explodedFilePath = explode('/', $filePath); | |
| $this->fileName = array_pop($explodedFilePath); | |
| if (strpos($filePath, 'ssh2.sftp://') === 0) { | |
| $this->ssh = new Ssh2SftpHelper($filePath); | |
| $this->filePath = "$this->storagePath/$this->fileName"; | |
| $this->ssh->download($this->filePath); | |
| } | |
| $this->file = fopen($this->filePath, 'r'); | |
| $this->fileSize = fstat($this->file)['size'] / 1024; | |
| } | |
| /** | |
| * @param bool $asArray | |
| * @return array|string | |
| */ | |
| public function getHeader($asArray = true) | |
| { | |
| $header = $asArray ? fgetcsv($this->file, 2000) : fgets($this->file); | |
| rewind($this->file); | |
| return $header; | |
| } | |
| /** | |
| * @param bool $includeHeader | |
| * @param bool $asArray | |
| * @return iterable | |
| */ | |
| public function getRows($includeHeader = false, $asArray = false) | |
| { | |
| if (! $includeHeader) | |
| fgets($this->file); | |
| while (! feof($this->file)) { | |
| yield $asArray ? fgetcsv($this->file, 10000) : fgets($this->file); | |
| } | |
| } | |
| /** | |
| * @return string | |
| */ | |
| public function getFileName() | |
| { | |
| return $this->fileName; | |
| } | |
| /** | |
| * @return float|int | |
| */ | |
| public function getFileSize() | |
| { | |
| return $this->fileSize; | |
| } | |
| /** | |
| * @return void | |
| */ | |
| public function close() | |
| { | |
| if (is_resource($this->file)) | |
| fclose($this->file); | |
| } | |
| /** | |
| * @return void | |
| */ | |
| public function __destruct() | |
| { | |
| $this->close(); | |
| if (! is_null($this->ssh)) | |
| unlink($this->filePath); | |
| } | |
| } |
| <?php | |
| namespace App\Classes; | |
| use Exception; | |
| class Ssh2SftpHelper | |
| { | |
| /** | |
| * @var $host string | |
| */ | |
| protected $host; | |
| /** | |
| * @var $remoteFilePath string | |
| */ | |
| protected $remoteFilePath; | |
| /** | |
| * @var $username string | |
| */ | |
| protected $username; | |
| /** | |
| * @var $password string | |
| */ | |
| protected $password; | |
| /** | |
| * @var $port int | |
| */ | |
| protected $port; | |
| /** | |
| * @var $connection resource | |
| */ | |
| protected $connection; | |
| public function __construct() | |
| { | |
| if (func_num_args() == 1) { | |
| $url = func_get_arg(0); | |
| if (strpos($url, 'ssh2.sftp://') === 0) { | |
| extract(parse_url($url)); | |
| $this->initialize($host, $path, $user, $pass); | |
| } else { | |
| throw new Exception('Please check your SFTP URL.'); | |
| } | |
| } else { | |
| $this->initialize(…func_get_args()); | |
| } | |
| } | |
| /** | |
| * @param string $host | |
| * @param string $filePath | |
| * @param string $username | |
| * @param string $password | |
| * @param int $port | |
| * @return void | |
| */ | |
| protected function initialize($host, $filePath = '', $username = '', $password = '', $port = 22) | |
| { | |
| $this->host = $host; | |
| $this->remoteFilePath = $filePath; | |
| $this->username = $username; | |
| $this->password = $password; | |
| $this->port = $port; | |
| } | |
| /** | |
| * @return void | |
| * @throws Exception | |
| */ | |
| protected function connect() | |
| { | |
| $this->connection = ssh2_connect($this->host, $this->port); | |
| if ($this->connection === false) | |
| throw new Exception("Unable to connect."); | |
| if (! is_null($this->username) && ! is_null($this->password)) { | |
| ssh2_auth_password($this->connection, $this->username, $this->password); | |
| } | |
| } | |
| /** | |
| * @param string $location | |
| * @return self | |
| */ | |
| public function remoteFile($location) | |
| { | |
| $this->remoteFilePath = $location; | |
| return $this; | |
| } | |
| /** | |
| * @param string $toDestination | |
| * @return bool | |
| * @throws Exception | |
| */ | |
| public function download($toDestination) | |
| { | |
| $this->connect(); | |
| if (is_null($this->remoteFilePath)) | |
| throw new Exception("Please identify the remote file to download."); | |
| return ssh2_scp_recv($this->connection, $this->remoteFilePath, $toDestination); | |
| } | |
| } |


Update Database After Job Completion
One last use case I’ve found for the __destruct method has been to update a database variable to indicate that a certain process was actually completed. Now of course, I understand that there are issues with this implementation if the database is not available, etc, etc. There are mitigation techniques that could be used to reduce the chances of this spiraling out of control, and assuming those are implemented, I believe this provides a pretty elegant way of ensuring that any script or process inheriting this __destruct implementation always denotes when it was completed.
For this example, I’m going to create a new table to track the scripts which are executed, when they were started and when they finished.
CREATE TABLE `scripts_table` (
`script_name` VARCHAR(50) NULL DEFAULT NULL,
`started_at` TIMESTAMP NULL DEFAULT NULL,
`completed_at` TIMESTAMP NULL DEFAULT NULL
)
COLLATE='utf8mb4_0900_ai_ci';
The changes to ParentImportScript are here.
Basically what I’ve done is in the constructor I create a timestamp indicating when the script is being executed and I also grab the script name. I arbitrarily chose 10 seconds to sleep the execution, then in the destructor I perform an update statement with the CURRENT_TIMESTAMP indicating the scripts completion. Below is the database result I saw:

I hope these use cases get the creative juices flowing and help you create your own.


Leave a Reply