Awesome Laravel: shift from legacy code to modern ✨ - by @tchuryy
In this post, we will refactor a legacy, badly written code, in order to see an example of refactoring. As an example, we will refactor a function that generates a dynamic image from an array containing data about text to write, rectangles to draw, and images to insert.
We will send using a POST request:
An array of options containing the background color and the horizontal margin;
An array of items, for example, text or images.
Route::get('/', function() {
$options = request()->post('options', [
'bg_color' => '#1e2565',
'margin_x' => 200
]);
$img = Image::canvas(1200, 675, $options['bg_color']);
$marginX = $options['margin_x'];
$availableWidth = 1200 - $marginX;
$items = request()->post('items', []);
foreach($items as $item) {
if($item['type'] === 'text' && isset($item['multiline']) && $item['multiline'] === true) {
$perLineChars = $availableWidth / $item['size'] * 2.1;
$exploded = explode(' ', $item['text']);
$lines = [];
$line = '';
foreach($exploded as $word) {
if(strlen($line) + strlen($word) < $perLineChars) {
$line .= $word . ' ';
} else {
$lines[] = $line;
$line = $word . ' ';
}
}
$lines[] = $line;
$currentY = $item['y'];
$spaceBetweenLines = $item['size'] + ($item['space_between_lines'] ?? 15);
foreach($lines as $line) {
$img->text($line, $item['x'], $currentY, function($font) use ($item) {
$font->file(base_path($item['font']));
$font->size($item['size']);
$font->color($item['color']);
$font->align($item['align']);
$font->valign($item['valign']);
});
$currentY += $spaceBetweenLines;
}
}
if($item['type'] === 'text' && (!isset($item['multiline']) || $item['multiline'] === false)) {
$img->text($item['text'], $item['x'], $item['y'], function($font) use ($item) {
$font->file(base_path($item['font']));
$font->size($item['size']);
$font->color($item['color']);
$font->valign($item['valign']);
$font->align($item['align']);
});
}
if($item['type'] === 'rectangle') {
$img->rectangle($item['x1'], $item['y1'], $item['x2'], $item['y2'], function($draw) use ($item) {
$draw->background($item['bg_color']);
});
}
if($item['type'] === 'image') {
$newImg = Image::make($item['base64']);
if(isset($item['widen'])) {
$newImg->widen($item['widen']);
}
$img->insert($newImg, $item['position'], $item['x'], $item['y']);
}
}
return $img->encode('data-url');
});
As you can see, is hard to read, but we can improve it. The refactoring’s purpose is to make the code not only readable but more reliable and efficient.
Example of item array
[{
'type': 'rectangle',
'x1': 100,
'y1': 490,
'x2': 1100,
'y2': 495,
'bg_color': '#e4f1ff',
},
{
'type': 'text',
'text': 'Alessandro',
'x': 160,
'y': 572,
'size': 36,
'space_between_lines': 15,
'color': '#e4f1ff',
'align': 'left',
'valign': 'bottom',
'font': '/public/Lato-Bold.ttf'
}]
Note: I will omit imports, but consider that we are using the Laravel Intervention package to handle the image generation.
Step 1: Extract the logic
As the first step, we can create a new class, called ImageGenerator to generate a new image. So, we create the class and we do a simple copy-paste. This is good because we can generate a new image from outside the routing function without copy-paste the code every time.
class ImageGenerator extends Image {
public static function generateImage($options, $items) {
$img = Image::canvas(1200, 675, $options['bg_color']);
$marginX = $options['margin_x'];
$availableWidth = 1200 - $marginX;
foreach($items as $item) {
if($item['type'] === 'text' && isset($item['multiline']) && $item['multiline'] === true) {
$perLineChars = $availableWidth / $item['size'] * 2.1;
$exploded = explode(' ', $item['text']);
$lines = [];
$line = '';
foreach($exploded as $word) {
if(strlen($line) + strlen($word) < $perLineChars) {
$line .= $word . ' ';
} else {
$lines[] = $line;
$line = $word . ' ';
}
}
$lines[] = $line;
$currentY = $item['y'];
$spaceBetweenLines = $item['size'] + ($item['space_between_lines'] ?? 15);
foreach($lines as $line) {
$img->text($line, $item['x'], $currentY, function($font) use ($item) {
$font->file(base_path($item['font']));
$font->size($item['size']);
$font->color($item['color']);
$font->align($item['align']);
$font->valign($item['valign']);
});
$currentY += $spaceBetweenLines;
}
}
if($item['type'] === 'text' && (!isset($item['multiline']) || $item['multiline'] === false)) {
$img->text($item['text'], $item['x'], $item['y'], function($font) use ($item) {
$font->file(base_path($item['font']));
$font->size($item['size']);
$font->color($item['color']);
$font->valign($item['valign']);
$font->align($item['align']);
});
}
if($item['type'] === 'rectangle') {
$img->rectangle($item['x1'], $item['y1'], $item['x2'], $item['y2'], function($draw) use ($item) {
$draw->background($item['bg_color']);
});
}
if($item['type'] === 'image') {
$newImg = Image::make($item['base64']);
if(isset($item['widen'])) {
$newImg->widen($item['widen']);
}
$img->insert($newImg, $item['position'], $item['x'], $item['y']);
}
}
return $img->encode('data-url');
}
}
Now, we have to call it into the routing function
Route::get('/', function() {
$options = request()->post('options', [
'bg_color' => '#1e2565',
'margin_x' => 200
]);
$items = request()->post('items', []);
return ImageGenerator::generateImage($options, $items);
});
🎯 Goals achieved
✅ Reusability.
Step 2: Use instances instead of static calls
We are calling a static function, but this is not good for many reasons, for example, testability. Furthermore, every generated image has some properties. So, we can add a constructor containing the default required parameters, e.g. options and items.
class ImageGenerator extends Image {
protected $options;
protected $items;
public function __construct($options, $items) { // options and items are part of the object now
$this->options = $options;
$this->items = $items;
}
public function generateImage($items) {
$img = Image::canvas(1200, 675, $this->options['bg_color']);
$marginX = $this->options['margin_x'];
$availableWidth = 1200 - $marginX;
foreach($this->items as $item) {
if($item['type'] === 'text' && isset($item['multiline']) && $item['multiline'] === true) {
$perLineChars = $availableWidth / $item['size'] * 2.1;
$exploded = explode(' ', $item['text']);
$lines = [];
$line = '';
foreach($exploded as $word) {
if(strlen($line) + strlen($word) < $perLineChars) {
$line .= $word . ' ';
} else {
$lines[] = $line;
$line = $word . ' ';
}
}
$lines[] = $line;
$currentY = $item['y'];
$spaceBetweenLines = $item['size'] + ($item['space_between_lines'] ?? 15);
foreach($lines as $line) {
$img->text($line, $item['x'], $currentY, function($font) use ($item) {
$font->file(base_path($item['font']));
$font->size($item['size']);
$font->color($item['color']);
$font->align($item['align']);
$font->valign($item['valign']);
});
$currentY += $spaceBetweenLines;
}
}
if($item['type'] === 'text' && (!isset($item['multiline']) || $item['multiline'] === false)) {
$img->text($item['text'], $item['x'], $item['y'], function($font) use ($item) {
$font->file(base_path($item['font']));
$font->size($item['size']);
$font->color($item['color']);
$font->valign($item['valign']);
$font->align($item['align']);
});
}
if($item['type'] === 'rectangle') {
$img->rectangle($item['x1'], $item['y1'], $item['x2'], $item['y2'], function($draw) use ($item) {
$draw->background($item['bg_color']);
});
}
if($item['type'] === 'image') {
$newImg = Image::make($item['base64']);
if(isset($item['widen'])) {
$newImg->widen($item['widen']);
}
$img->insert($newImg, $item['position'], $item['x'], $item['y']);
}
}
return $img->encode('data-url');
}
}
In this way, we have to create a new instance of ImageGenerator, and return the generated image.
Route::get('/', function() {
$options = request()->post('options', [
'bg_color' => '#1e2565',
'margin_x' => 200
]);
$items = request()->post('items', []);
$imageGenerator = new ImageGenerator($options, $items);
return $imageGenerator->generateImage();
});
Now we have demanded the process to a new ImageGenerator object, and we can generate it as many times as we want.
🎯 Goals achieved
✅ Call a function on the same object multiple times.
Step 3: Manage default values
In the generateImage function, we now assume that $options['bg_color'] exists, otherwise we will have an exception. In fact, this assumption is wrong because a user can submit an array of $options containing only the key margin_x. In order to handle this, I use to have every time a variable containing default values and I merge it with the received parameter to avoid not existing keys. So we are sure every key will have value.
In this case, $options should contain two keys: bg_color and margin_x. I’m also adding types and default values for received parameters.
class ImageGenerator extends Image {
const DEFAULT_IMAGE_OPTIONS = ['bg_color' => '#1e2565', 'margin_x' => 200];
protected array $options;
protected array $items;
public function __construct(array $options = [], array $items = []) {
// merging default options with received
$this->options = array_merge(self::DEFAULT_IMAGE_OPTIONS, $this->options);
$this->items = $items;
}
public function generateImage(array $items) {
$img = Image::canvas(1200, 675, $this->options['bg_color']);
$marginX = $this->options['margin_x'];
$availableWidth = 1200 - $marginX;
foreach($this->items as $item) {
if($item['type'] === 'text' && isset($item['multiline']) && $item['multiline'] === true) {
$perLineChars = $availableWidth / $item['size'] * 2.1;
$exploded = explode(' ', $item['text']);
$lines = [];
$line = '';
foreach($exploded as $word) {
if(strlen($line) + strlen($word) < $perLineChars) {
$line .= $word . ' ';
} else {
$lines[] = $line;
$line = $word . ' ';
}
}
$lines[] = $line;
$currentY = $item['y'];
$spaceBetweenLines = $item['size'] + ($item['space_between_lines'] ?? 15);
foreach($lines as $line) {
$img->text($line, $item['x'], $currentY, function($font) use ($item) {
$font->file(base_path($item['font']));
$font->size($item['size']);
$font->color($item['color']);
$font->align($item['align']);
$font->valign($item['valign']);
});
$currentY += $spaceBetweenLines;
}
}
if($item['type'] === 'text' && (!isset($item['multiline']) || $item['multiline'] === false)) {
$img->text($item['text'], $item['x'], $item['y'], function($font) use ($item) {
$font->file(base_path($item['font']));
$font->size($item['size']);
$font->color($item['color']);
$font->valign($item['valign']);
$font->align($item['align']);
});
}
if($item['type'] === 'rectangle') {
$img->rectangle($item['x1'], $item['y1'], $item['x2'], $item['y2'], function($draw) use ($item) {
$draw->background($item['bg_color']);
});
}
if($item['type'] === 'image') {
$newImg = Image::make($item['base64']);
if(isset($item['widen'])) {
$newImg->widen($item['widen']);
}
$img->insert($newImg, $item['position'], $item['x'], $item['y']);
}
}
return $img->encode('data-url');
}
}
Before the next step, I want to observe that the width and the height should be a property of the ImageGenerator, because it can change between two images. So, I’m adding it to the constructor. I will do the same for the img, because an instance of ImageGenerator should manage only one image.
class ImageGenerator extends Image {
const DEFAULT_IMAGE_OPTIONS = ['bg_color' => '#1e2565', 'margin_x' => 200];
protected int $width;
protected int $height;
protected array $options;
protected array $items;
protected Image $img;
public function __construct(int $width = 1200, int $height = 675, array $options = [], array $items = []) {
$this->width = $width;
$this->height = $height;
// merging default options with received one
$this->options = array_merge(self::DEFAULT_IMAGE_OPTIONS, $this->options);
$this->items = $items;
$this->img = Image::canvas($this->width, $this->height, $this->options['bg_color']);
}
public function generateImage(array $items) {
$marginX = $this->options['margin_x'];
$availableWidth = $this->width - $marginX;
foreach($this->items as $item) {
if($item['type'] === 'text' && isset($item['multiline']) && $item['multiline'] === true) {
$perLineChars = $availableWidth / $item['size'] * 2.1;
$exploded = explode(' ', $item['text']);
$lines = [];
$line = '';
foreach($exploded as $word) {
if(strlen($line) + strlen($word) < $perLineChars) {
$line .= $word . ' ';
} else {
$lines[] = $line;
$line = $word . ' ';
}
}
$lines[] = $line;
$currentY = $item['y'];
$spaceBetweenLines = $item['size'] + ($item['space_between_lines'] ?? 15);
foreach($lines as $line) {
$this->img->text($line, $item['x'], $currentY, function($font) use ($item) {
$font->file(base_path($item['font']));
$font->size($item['size']);
$font->color($item['color']);
$font->align($item['align']);
$font->valign($item['valign']);
});
$currentY += $spaceBetweenLines;
}
}
if($item['type'] === 'text' && (!isset($item['multiline']) || $item['multiline'] === false)) {
$this->img->text($item['text'], $item['x'], $item['y'], function($font) use ($item) {
$font->file(base_path($item['font']));
$font->size($item['size']);
$font->color($item['color']);
$font->valign($item['valign']);
$font->align($item['align']);
});
}
if($item['type'] === 'rectangle') {
$this->img->rectangle($item['x1'], $item['y1'], $item['x2'], $item['y2'], function($draw) use ($item) {
$draw->background($item['bg_color']);
});
}
if($item['type'] === 'image') {
$newImg = Image::make($item['base64']);
if(isset($item['widen'])) {
$newImg->widen($item['widen']);
}
$this->img->insert($newImg, $item['position'], $item['x'], $item['y']);
}
}
return $this->img->encode('data-url');
}
}
The routing function will be modified as follows.
Route::get('/', function() {
$options = request()->post('options', [
'bg_color' => '#1e2565',
'margin_x' => 200
]);
$items = request()->post('items', []);
$imageGenerator = new ImageGenerator(1200, 675, $options, $items);
return $imageGenerator->generateImage();
});
🎯 Goals achieved
✅ Extraction of instance properties;
✅ Default variables values handling.
The function is not readable at all, but we can now do more things.
Step 4: Splitting the logic
Our ImageGenerator contains only one method: generateImage.
It is a very long function, and if we will need to edit after some months or years we will not remember everything written. So my advice is to split the logic into multiple functions. For example, we do a foreach on items to add text, images, or rectangles. We can replace his behavior easily by introducing functions such as addImage. In this way, we have a dedicated function with a clear name that do a single thing.
class ImageGenerator extends Image {
const DEFAULT_IMAGE_OPTIONS = ['bg_color' => '#1e2565', 'margin_x' => 200];
protected int $width;
protected int $height;
protected array $options;
protected array $items;
protected Image $img;
public function __construct(int $width = 1200, int $height = 675, array $options = [], array $items = []) {
// merging default options with received
$this->width = $width;
$this->height = $height;
$this->options = array_merge(self::DEFAULT_IMAGE_OPTIONS, $this->options);
$this->items = $items;
$this->img = Image::canvas($this->width, $this->height, $this->options['bg_color']);
}
public function generateImage(array $items) {
$marginX = $this->options['margin_x'];
$availableWidth = $this->width - $marginX;
foreach($this->items as $item) {
if($item['type'] === 'text') {
$this->addText($item);
}
if($item['type'] === 'rectangle') {
$this->addRectangle($item);
}
if($item['type'] === 'image') {
$this->addImage($item);
}
}
return $this->img->encode('data-url');
}
// function to write text
public function addText($item) {
if($item['type'] === 'text' && isset($item['multiline']) && $item['multiline'] === true) {
$perLineChars = $availableWidth / $item['size'] * 2.1;
$exploded = explode(' ', $item['text']);
$lines = [];
$line = '';
foreach($exploded as $word) {
if(strlen($line) + strlen($word) < $perLineChars) {
$line .= $word . ' ';
} else {
$lines[] = $line;
$line = $word . ' ';
}
}
$lines[] = $line;
$currentY = $item['y'];
$spaceBetweenLines = $item['size'] + ($item['space_between_lines'] ?? 15);
foreach($lines as $line) {
$this->img->text($line, $item['x'], $currentY, function($font) use ($item) {
$font->file(base_path($item['font']));
$font->size($item['size']);
$font->color($item['color']);
$font->align($item['align']);
$font->valign($item['valign']);
});
$currentY += $spaceBetweenLines;
}
}
if($item['type'] === 'text' && (!isset($item['multiline']) || $item['multiline'] === false)) {
$this->img->text($item['text'], $item['x'], $item['y'], function($font) use ($item) {
$font->file(base_path($item['font']));
$font->size($item['size']);
$font->color($item['color']);
$font->valign($item['valign']);
$font->align($item['align']);
});
}
}
// function to insert rectangles
public function addRectangle($item) {
$this->img->rectangle($item['x1'], $item['y1'], $item['x2'], $item['y2'], function($draw) use ($item) {
$draw->background($item['bg_color']);
});
}
// function to insert images
public function addImage($item) {
$newImg = Image::make($item['base64']);
if(isset($item['widen'])) {
$newImg->widen($item['widen']);
}
$this->img->insert($newImg, $item['position'], $item['x'], $item['y']);
}
}
Now, we are not touching the routing function, but the code seems way more clear than before. I know this can be seen as a time-consuming task, but I guarantee you will love this when you will need to work again on this code.
🎯 Goals achieved
✅ One function for each item type;
✅ Readability improved.
Step 5: Calculate on construct things you will ever need
Let’s analyze the code: We have, in our generateImage function, this code: $availableWidth = $this->width — $marginX. This represents the area of images we want to work on excluding the horizontal margin and it will not change throughout the object lifecycle. I’m also adding a condition to avoid negative values.
Furthermore, the items array is used only once, e.g. when we want to generate the image. But if we assume the items in an image will remain the same, we can move it into the constructor.
class ImageGenerator extends Image {
const DEFAULT_IMAGE_OPTIONS = ['bg_color' => '#1e2565', 'margin_x' => 200];
protected int $width;
protected int $height;
protected array $options;
protected array $items;
protected int $availableWidth;
protected Image $img;
public function __construct(int $width = 1200, int $height = 675, array $options = [], array $items = []) {
// merging default options with received
$this->width = $width;
$this->height = $height;
$this->options = array_merge(self::DEFAULT_IMAGE_OPTIONS, $this->options);
$this->items = $items;
$this->availableWidth = $this->width > $this->options['margin_x'] ? $this->width - $this->options['margin_x'] : 0;
$this->img = Image::canvas($this->width, $this->height, $this->options['bg_color']);
foreach($this->items as $item) {
if($item['type'] === 'text') {
$this->addText($item);
}
if($item['type'] === 'rectangle') {
$this->addRectangle($item);
}
if($item['type'] === 'image') {
$this->addImage($item);
}
}
}
public function generateImage(array $items) {
$marginX = $this->options['margin_x'];
return $this->img->encode('data-url');
}
public function addText($item) {
if($item['type'] === 'text' && isset($item['multiline']) && $item['multiline'] === true) {
$perLineChars = $this->availableWidth / $item['size'] * 2.1;
$exploded = explode(' ', $item['text']);
$lines = [];
$line = '';
foreach($exploded as $word) {
if(strlen($line) + strlen($word) < $perLineChars) {
$line .= $word . ' ';
} else {
$lines[] = $line;
$line = $word . ' ';
}
}
$lines[] = $line;
$currentY = $item['y'];
$spaceBetweenLines = $item['size'] + ($item['space_between_lines'] ?? 15);
foreach($lines as $line) {
$this->img->text($line, $item['x'], $currentY, function($font) use ($item) {
$font->file(base_path($item['font']));
$font->size($item['size']);
$font->color($item['color']);
$font->align($item['align']);
$font->valign($item['valign']);
});
$currentY += $spaceBetweenLines;
}
}
if($item['type'] === 'text' && (!isset($item['multiline']) || $item['multiline'] === false)) {
$this->img->text($item['text'], $item['x'], $item['y'], function($font) use ($item) {
$font->file(base_path($item['font']));
$font->size($item['size']);
$font->color($item['color']);
$font->valign($item['valign']);
$font->align($item['align']);
});
}
}
public function addRectangle($item) {
$this->img->rectangle($item['x1'], $item['y1'], $item['x2'], $item['y2'], function($draw) use ($item) {
$draw->background($item['bg_color']);
});
}
public function addImage($item) {
$newImg = Image::make($item['base64']);
if(isset($item['widen'])) {
$newImg->widen($item['widen']);
}
$this->img->insert($newImg, $item['position'], $item['x'], $item['y']);
}
}
🎯 Goals achieved
✅ Performance improved: calculation is done at creation time, so only one time.
Step 6: Take advantage of the language
In PHP, we can use enums to provide a set of possible values. In our case, it would be helpful to have an enum for the item type (text, image…).
Additionally, I’m creating a function defaultOptions which returns the array of default options for a given item type. We will integrate this enum in the next step.
enum ItemsType: string {
case TEXT = 'text';
case MULTILINE_TEXT = 'multiline_text';
case RECTANGLE = 'rectangle';
case IMAGE = 'image';
function defaultOptions(): arra
return match ($this) {
self::TEXT => [
'text' => '', 'x' => 0, 'y' => 0, 'font' => '/public/Lato-Bold.ttf', 'size' => 12,
'color' => '#000000', 'align' => 'center', 'valign' => 'top',
],
// MULTILINE_TEXT has TEXT properties plus others, so I will take exactly TEXT default options
self::MULTILINE_TEXT => array_merge(self::TEXT->defaultOptions(), [
'space_between_lines' => 15, 'per_line_chars' => 30,
]),
self::RECTANGLE => ['x1' => 0, 'y1' => 0, 'x2' => 10, 'y2' => 10, 'bg_color' => '#000000'],
self::IMAGE => ['base64' => '', 'position' => 'top-right', 'x' => 0, 'y' => 0],
};
}
}
🎯 Goals achieved
✅ Reliability: code knows the set of possible values;
✅ Readability: you can look at the enum to see possible values.
Step 7: Optimize functions
This is a crucial step. Now the code is more readable, but we want to power up speed, performance, and reliability. In order to do this, we can look at every function and see if there is something to improve. This task requires experience because most of the time you will understand if a function is improvable intuitively. Furthermore, we can use what our programming language provides to write better code, such as helper functions.
In our case, the addText function is a bit weird, and in fact, we see two main tasks:
If an item has multiline=true, then write a multiline text, with a function to calculate how many chars per line;
Otherwise, write a text.
Both tasks aim to write text, so why we can’t create a dedicated function for this? And I will now replace some parts of the code using functions provided by the language.
class ImageGenerator extends Image {
const DEFAULT_IMAGE_OPTIONS = ['bg_color' => '#1e2565', 'margin_x' => 200];
protected int $imageWidthWithoutMargin = 0; // renamed from availableWidth
protected \Intervention\Image\Image $image; // renamed from img
public function __construct(
protected int $width = 1200,
protected int $height = 675,
protected array $options = [],
protected array $items = []
) {
$this->options = array_merge(self::DEFAULT_IMAGE_OPTIONS, $this->options);
$this->image = Image::canvas($this->width, $this->height, $this->options['bg_color']);
$this->imageWidthWithoutMargin = $this->width > $this->options['margin_x'] ? $this->width - $this->options['margin_x'] : 0;
array_map(fn($item) => $this->addItem($item), $items); // instead of foreach
}
// manage the addition of an item
protected function addItem(array $item): void {
// match instead of multiple if
match ($item['type']) {
ItemsType::TEXT->value => $this->addText($item),
ItemsType::RECTANGLE->value => $this->addRectangle($item),
ItemsType::IMAGE->value => $this->addImage($item),
default => null,
};
}
// before it was really difficult to understand
protected function addText(array $item): void {
// a text without multiline key will be treated as a single line text
match ($item['multiline'] ?? false) {
true => $this->addMultilineText($item),
false => $this->addSinglelineText($item),
};
}
protected function addMultilineText(array $item): void {
$item = array_merge(ItemsType::MULTILINE_TEXT->defaultOptions(), $item);
$lines = $this->splitTextInLines($item['text'], $item['per_line_chars']);
$currentY = $item['y'];
$lineHeight = $item['size'] + $item['space_between_lines'];
foreach ($lines as $line) {
// we use writeText to write both single and multi line text
$this->writeText(array_merge($item, ['text' => $line, 'y' => $currentY]));
$currentY += $lineHeight;
}
}
// extracted function from addMultilineText
protected function splitTextInLines(string $text, int $perLineChars): array {
return explode("\n", wordwrap($text, $perLineChars));
}
protected function addSinglelineText(array $item): void {
$item = array_merge(ItemsType::TEXT->defaultOptions(), $item);
$this->writeText($item);
}
protected function writeText(array $item = []) {
$this->image->text($item['text'], $item['x'], $item['y'], function ($font) use ($item) {
$font->file(base_path($item['font']));
$font->size($item['size']);
$font->color($item['color']);
$font->valign($item['valign']);
$font->align($item['align']);
});
}
protected function addRectangle(array $item = []): void {
$item = array_merge(ItemsType::RECTANGLE->defaultOptions(), $item);
$this->image->rectangle($item['x1'], $item['y1'], $item['x2'], $item['y2'], function ($draw) use ($item) {
$draw->background($item['bg_color']);
});
}
protected function addImage(array $item = []): void {
$item = array_merge(ItemsType::IMAGE->defaultOptions(), $item);
// we have now a dedicated function to create sub images
$subImage = $this->createSubImage($item['base64'], $item['widen'] ?? null);
$this->image->insert($subImage, $item['position'], $item['x'], $item['y']);
}
protected function createSubImage(string $base64 = '', $widen = null): \Intervention\Image\Image {
$subImage = Image::make($base64);
if ($widen) $subImage->widen($widen);
return $subImage;
}
// instead of generateImage, this name sounds more clear
public function getImageEncoded(string $format = 'data-url'): \Intervention\Image\Image {
return $this->image->encode($format);
}
}
We changed the name of generateImage into getImageEncoded because now we have the image and we want only to get the image in a given format. So we have to change the routing function, and we can also move it into a single action controller.
class GenerateImageController extends Controller {
public function __invoke() {
$options = request()->post('options', []);
$items = request()->post('items', []);
$imageGenerator = new ImageGenerator(options: $options, items: $items);
return response($imageGenerator->getImageEncoded());
}
}
I have already used named arguments to call the ImageGenerator constructor because it makes the code more readable.
🎯 Goals achieved
✅ Used functions provided by language to improve;
✅ Controller extraction.
🎁 Bonus step: Use measure unit in variables
Sometimes, we have to deal with measure units, like averageTimeInSeconds or getAreaInSquareMeters. In our case, we have width, height, and other variables where we can express the measuring unit. Let’s fix this.
class ImageGenerator extends Image {
const DEFAULT_IMAGE_OPTIONS = ['bg_color' => '#1e2565', 'margin_x' => 200];
protected int $imageWidthWithoutMarginInPx = 0; // renamed from availableWidth
protected \Intervention\Image\Image $image; // renamed from img
public function __construct(
protected int $widthInPx = 1200, // added "inPx"
protected int $heightInPx = 675,
protected array $options = [],
protected array $items = []
) {
$this->options = array_merge(self::DEFAULT_IMAGE_OPTIONS, $this->options);
$this->image = Image::canvas($this->width, $this->heightInPx, $this->options['bg_color']);
$this->imageWidthWithoutMarginInPx = $this->widthInPx > $this->options['margin_x'] ? $this->width - $this->options['margin_x'] : 0;
array_map(fn($item) => $this->addItem($item), $items); // instead of foreach
}
// manage the addition of an item
protected function addItem(array $item): void {
// match instead of multiple if
match ($item['type']) {
ItemsType::TEXT->value => $this->addText($item),
ItemsType::RECTANGLE->value => $this->addRectangle($item),
ItemsType::IMAGE->value => $this->addImage($item),
default => null,
};
}
// before it was really difficult to understand
protected function addText(array $item): void {
// a text without multiline key will be treated as a single line text
match ($item['multiline'] ?? false) {
true => $this->addMultilineText($item),
false => $this->addSinglelineText($item),
};
}
protected function addMultilineText(array $item): void {
$item = array_merge(ItemsType::MULTILINE_TEXT->defaultOptions(), $item);
$lines = $this->splitTextInLines($item['text'], $item['per_line_chars']);
$currentY = $item['y'];
$lineHeightInPx = $item['size'] + $item['space_between_lines'];
foreach ($lines as $line) {
// we use writeText to write both single and multi line text
$this->writeText(array_merge($item, ['text' => $line, 'y' => $currentY]));
$currentY += $lineHeightInPx;
}
}
// extracted function
protected function splitTextInLines(string $text, int $perLineChars): array {
return explode("\n", wordwrap($text, $perLineChars));
}
protected function addSinglelineText(array $item): void {
$item = array_merge(ItemsType::TEXT->defaultOptions(), $item);
$this->writeText($item);
}
protected function writeText(array $item = []) {
$this->image->text($item['text'], $item['x'], $item['y'], function ($font) use ($item) {
$font->file(base_path($item['font']));
$font->size($item['size']);
$font->color($item['color']);
$font->valign($item['valign']);
$font->align($item['align']);
});
}
protected function addRectangle(array $item = []): void {
$item = array_merge(ItemsType::RECTANGLE->defaultOptions(), $item);
$this->image->rectangle($item['x1'], $item['y1'], $item['x2'], $item['y2'], function ($draw) use ($item) {
$draw->background($item['bg_color']);
});
}
protected function addImage(array $item = []): void {
$item = array_merge(ItemsType::IMAGE->defaultOptions(), $item);
// we have now a dedicated function to create sub images
$subImage = $this->createSubImage($item['base64'], $item['widen'] ?? null);
$this->image->insert($subImage, $item['position'], $item['x'], $item['y']);
}
protected function createSubImage(string $base64 = '', $widenInPx = null): \Intervention\Image\Image {
$subImage = Image::make($base64);
if ($widenInPx) $subImage->widen($widenInPx);
return $subImage;
}
// instead of generateImage, this name sounds more clear
public function getImageEncoded(string $format = 'data-url'): \Intervention\Image\Image {
return $this->image->encode($format);
}
}
✅ Congratulations! We refactored complex code into readable, reusable, and optimized code!
Now, we can continue to improve the code, but I think we reached an awesome result. I prefer to stop when I reach a level I like.
I think refactoring should be a fundamental part of a developer's work and, if you’re new to development, I absolutely recommend mastering this concept. You will be a better dev.
Actually, I don’t know, but I can make a second part if there will be someone interested 👀, so if you liked this article, feel free to share it :)
P.S.: yes, the sharing image was made by this code.