Look at a typical error message from a general Carbon Exception:
Wouldn't it be better if we showed a more specific error message?
We can build and catch custom exceptions for this.
Here are possible reasons:
ImportHasMalformedData
or UserAlreadyExists
.The import has malformed data.
or The user already exists.
.string
with a value (see example below).To illustrate this, let's look at the following code:
$error = 'OK'; function workWithFile($file) { // Reads the File... // Works with File... // Possibly has an error inside.} $result = workWithFile('PATH_TO_FILE'); if ($result == 'NOT_FOUND') { deleteFriendListFromMenu();} elseif ($result == 'NOT_READABLE') { alertUserAboutPermissionProblem();} elseif ($result == 'OK') { return $result; // File was read okay!} else { die("Something bad happened. Not in the list")}
Looking at this code, we might understand what is going on, but there are a lot of hard-coded strings. So, we need to remember those strings' format and avoid making any typos in them.
If we make a typo somewhere, PHP/Laravel/IDE won't immediately flag it as a bug.
Using Exceptions can be a better experience:
function workWithFile($file) { if (!is_file($file)) { throw new FileNotFoundException($file); } else if (!is_readable($file)) { throw new FileNotReadableException($file); } else { // Reads the File... }} try { $fileContents = workWithFile('PATH_TO_FILE'); return $fileContents;} catch (FileNotFoundException $e) { //File not found, we can log it, alert the user, etc.} catch (FileNotReadableException $e) { //File could not be read; we can log it, alert the user, etc.}
As you can see, we dropped quite a few if
conditions and instead used different exceptions to handle the errors. This makes the code more readable and easier to understand, especially if combined with custom exceptions.
Also, if we make a typo in the class name, PHP/IDE will immediately flag it as a bug before the code is pushed to live.
To create a custom Exception in Laravel, you need to run a single command:
php artisan make:exception ImportHasMalformedData
It will create a new file in app/Exceptions/ImportHasMalformedData.php
:
app/Exceptions/ImportHasMalformedData.php
class ImportHasMalformedData extends Exception{ protected $message = 'The import has malformed data.';}
Now throwing this Exception will result in a more actionable message:
This Exception can now be caught by the catch
and handled accordingly. For example, we'll display a message to the user using this code:
use App\Exceptions\ImportHasMalformedData; // ... public function __invoke(){ $data = [['name' => 'John Doe', 'email' => 'email@email.com', 'date' => '23-04']]; try { $this->import($data); } catch (ImportHasMalformedData $e) { return 'Malformed data'; } return 'Imported';}
Running this, we will have the following output in our browser:
Malformed data
To go even further, we can add more exceptions to our code that will better describe the problem:
php artisan make:exception ImportFailedToParseDatephp artisan make:exception ImportFailedToParseEmailphp artisan make:exception ImportHasNoNamephp artisan make:exception ImportHasNoCompanyphp artisan make:exception ImportHasEmptyRow
Now, we can use them in our code:
use App\Exceptions\ImportFailedToParseDate;use App\Exceptions\ImportFailedToParseEmail;use App\Exceptions\ImportHasNoCompany;use App\Exceptions\ImportHasNoName;use App\Exceptions\ImportHasEmptyRow; // ... public function __invoke() { // Load the file $failedRows = []; foreach($fileData as $row) { try { $this->importRow($row); } catch (ImportFailedToParseDate $e) { // Handle the error $failedRows[get_class($e)][] = $row; } catch (ImportFailedToParseEmail $e) { // Handle the error $failedRows[get_class($e)][] = $row; } catch (ImportHasNoName $e) { // Handle the error $failedRows[get_class($e)][] = $row; } catch (ImportHasNoCompany $e) { // Handle the error $failedRows[get_class($e)][] = $row; } catch (ImportHasEmptyRow $e) { // Handle the error continue; // Skip this row and continue with the next one } catch (Exception $e) { $failedRows[$e->getMessage()][] = $row; // Handle any other errors } } // Email user with failed rows (or display preview) // ... return 'Imported';} private function importRow(array $rowData) { if (empty($rowData)) { throw new ImportHasEmptyRow(); } if (!isset($rowData['name'])) { throw new ImportHasNoName(); } if (!isset($rowData['company'])) { throw new ImportHasNoCompany(); } if (!isset($rowData['email'])) { throw new ImportFailedToParseEmail(); } if (!isset($rowData['date'])) { throw new ImportFailedToParseDate(); } try { $date = Carbon::parse($rowData['date']); } catch (Exception $e){ throw new ImportFailedToParseDate(previous: $e); } // Import the row to the database}
This allows us to have a more structured code and quickly understand what is happening. Especially since we know that if any file contents are malformed, we will get an Exception with specific information about the problem.
Notice: You may do different things in the catch
block, like redirecting the user to a separate page, logging the error, etc.