Engineer’s Log, Stardate 98821.1:
Our ship has made contact with an alien species known as the Error. So far, we’ve only been able to decipher vague sentiments from our communications with these beings. Tensions are high, and the crew is at Red Alert until we can establish true first contact and ascertain the creature’s intentions. On the Captain’s orders, I have rerouted the alien signal to my engineering station and have developed a fractal algorithm, which I believe will allow us to translate the alien’s language with greater accuracy.
NB: I frequently use the term “Error” in this article to colloquially refer to “Throwables”, which technically encapsulates both Errors and Exceptions.
The Issue
During a recent client engagement, I was developing a Connector to import Microsoft AX data into Akeneo. The connector would allow Sitation’s client to easily upload CSV exports from Microsoft AX into a piece of middleware, which would then handle some light validation as well as the data transformation and import into Akeneo. However, as I progressed, I was struggling to determine the cause of several Akeneo PHP errors due to the generality of the main exception being thrown: Akeneo\Pim\ApiClient\Exception\UnprocessableEntityHttpException
Stack trace:
#0 /home/aidan/Documents/akeneoimporter/vendor/akeneo/api-php-client/src/Client/HttpClient.php(62): Akeneo\Pim\ApiClient\Client\HttpExceptionHandler->transformResponseToException()
#1 /home/aidan/Documents/akeneoimporter/vendor/akeneo/api-php-client/src/Client/AuthenticatedHttpClient.php(63): Akeneo\Pim\ApiClient\Client\HttpClient->sendRequest()
#2 /home/aidan/Documents/akeneoimporter/vendor/akeneo/api-php-client/src/Client/ResourceClient.php(142): Akeneo\Pim\ApiClient\Client\AuthenticatedHttpClient->sendRequest()
#3 /home/aidan/Documents/akeneoimporter/vendor/akeneo/api-php-client/src/Api/ProductApi.php(97): Akeneo\Pim\ApiClient\Client\ResourceClient->upsertResource()
#4 /home/aidan/Documents/akeneoimporter/Importer.php(92): Akeneo\Pim\ApiClient\Api\ProductApi->upsert()
#5 /home/aidan/Documents/akeneoimporter/ImporterRunner.php(24): Importer->importAxData()
#6 {main}
It is clear from this information that our issue occurs when we attempt to upload new data into Akeneo, and that the data is invalid in some way. However, an examination of the invalid data provided no further insight into the possible cause. I decided to allocate time to expand our error logging so that I could better understand the source of the error. Additionally, there were a number of errors I wanted to log which were fatal errors. Fatal errors cause the process to shutdown after being thrown, so logging them before that shutdown was the crucial challenge. The error logging recommendations described below solve this problem as well.
Logging Recommendations
Initial Logging
First, I wrote a function to encapsulate general error logging functionality, and to begin adding some useful information to the logs, such as a timestamp or a SKU of the invalid product (if relevant). For the purposes of this example, we will log to a text file at the root of our project named error.log. /** * Log Throwables and Custom Errors
* @param Mixed $customText
* @param Throwable $e
* @param String $sku
*
*/
private function logError($customText, Throwable $e=NULL, String $sku='N/A'){
// Format and get the timestamp
date_default_timezone_set('UTC');
$logDateTime = date(DATE_ATOM);
// Construct the log string and write to log. Format the output as you like.
$errorLog = fopen('error.log', 'a');
$error = (isset($e)) ? $e : $customText;
$logText = "SKU: " . $sku . " | Timestamp (UTC): " . $logDateTime . " | Error: " . $error . "\n\n";
fwrite($errorLog, $logText);
fclose($errorLog);
}
The function allows the developer to either catch and log a Throwable, or to log a custom error string. Depending on the error, a SKU can also be passed for more specific logging. Here are real examples of how both functionalities can be used: // Log a throwable
$filePath = 'Import_Test.xlsx';
try {
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($filePath);
} catch (Throwable $e){
$this->logError(NULL, $e);
}
// Log a custom String, marking the end of some process
$this->logError('END: '.date('Y-m-d H:i:s'));
// Log a throwable and the SKU
$this->logError(NULL, $e, $product['identifier']);
With this function, our errors now look like so: SKU: E-185-00227 | Timestamp (UTC): 2021-03-19T05:13:48+00:00 | Error: Akeneo\Pim\ApiClient\Exception\UnprocessableEntityHttpException: Validation failed. (see https://api.akeneo.com/php-client/exception.html#unprocessable-entity-exception) in /home/aidan/Documents/akeneoimporter/vendor/akeneo/api-php-client/src/Client/HttpExceptionHandler.php:58
Stack trace:
// etc.
Fatal Errors
The next step was to catch and report fatal errors, which cause application shutdown. After a bit of research, I discovered that whenever a PHP application shuts down, it runs a shutdown function if such a function has been registered. So, my approach was to register a shutdown function in the main application file, which has the same general functionality as the logError() function. Now, when a fatal error occurs, the program runs the shutdown function and logs the last error before shutdown. This function also runs on normal application shutdown (e.g. when the run is completed), which is why we check that the result of error_get_last() is not null before proceeding. function shutDownFunction() {
$error = error_get_last();
if (isset($error) && $error['type'] === E_ERROR) {
date_default_timezone_set('UTC');
$logDateTime = date(DATE_ATOM);
// Construct the log string as you wish and write to log
$errorLog = fopen('error.log', 'a');
$logText = "SKU: N/A | Timestamp (UTC): " . $logDateTime . " | Error: " . $error['message'] . "\n\n";
fwrite($errorLog, $logText);
fclose($errorLog);
}
}
register_shutdown_function('shutDownFunction');
Akeneo Error Specifics
The last two steps set up some clean and readable error logging for the importer, however the Akeneo errors themselves remain vague. After finding Akeneo’s documentation on their custom errors (https://api.akeneo.com/php-client/exception.html), I expanded our error logging function to include the array of sub-errors returned with an UnprocessableEntityHttpException. private function logError($customText, Throwable $e=NULL, String $sku='N/A'){
date_default_timezone_set('UTC');
$logDateTime = date(DATE_ATOM);
$errorLog = fopen('error.log', 'a');
$detail = "N/A";
if(isset($e)){
// Get more info out of akeneo errors
if(strpos(get_class($e), "UnprocessableEntityHttpException") !== false){
$detail = "";
foreach($e->getResponseErrors() as $err){
$detail .= "\nProperty: " . $err['property'] . " - Message: " . $err['message'];
}
}
$error = $e;
} else {
$error = $customText;
}
$logText = "SKU: " . $sku . " | Timestamp (UTC): " . $logDateTime . " | Error: " . $error . "\nDetail: " . $detail . "\n\n";
fwrite($errorLog, $logText); fclose($errorLog);
}
Finally, the last two lines of our error includes some very useful information: Detail:
Property: quantifiedAssociations.kit.products[1].quantity - Message: "0" is an invalid quantity. Please, write a value between 1 and 2147483
Engineer’s Log, Stardate 98821.78:
After many hours, my algorithm was successful in deciphering the alien’s language. We have established a fluent dialogue with the one they call User Error, who, our scans tell us, is by far the most ancient being we’ve encountered in this quadrant. With the knowledge we gained through an exchange of information and culture, our Captain feels confident that we will be able to pass through this sector of Error-controlled space without any further issues.
The Akeneo PHP SDK can be used to perform one-time imports of custom data, to build middleware for scheduled data syncs, to develop custom tools for complex operations not supported by the platform, and much more. Regardless of what you’re trying to build, now you know how to set up some simple, initial logging for a PHP application connecting to Akeneo. I hope that this article can save you some time and headaches on future projects!