PHP code refactoring in VSCode
Currently, end of February 2024, there aren't many free refactoring extensions for PHP under VSCode. We'll take a look to PHP Refactor Tool and PHP Refactoring.
We'll learn how to rename a symbol, a class and, even better, how to extract a portion of a long method into a new one.
Create sample filesโ
For the demo, please start a Linux shell and run mkdir -p /tmp/refactor && cd $_
to create a folder called refactor
in your Linux temporary folder and jump in it.
Please create two php files. The first one will be called index.php
and will contain this code:
<?php
namespace Cavo;
require_once('Product.php');
$product = new Product();
$product->setProductName('keyboard');
$product->setProductPrice(125);
printf(
'The name is %s cost %d โฌ'.PHP_EOL,
$product->getProductName(),
$product->getProductPrice()
);
The second file Product.php
will contain:
<?php
namespace Cavo;
class Product
{
private string $productName = '';
private float $productPrice = 0;
public function __construct(string $name='', float $price=0)
{
$this->setProductName($name);
$this->setProductPrice($price);
}
public function getProductName(): string {
return $this->productName;
}
public function setProductName(string $name): void {
$this->productName = $name;
}
public function getProductPrice(): float {
return $this->productPrice;
}
public function setProductPrice(float $price): void {
$this->productPrice = $price;
}
}
Run the exampleโ
Run docker run -it --rm -v "${PWD}":/project -w /project php:8.2 php index.php
to run our example in the console.
You'll get, as expected:
โฏ docker run -it --rm -v "$PWD":/project -w /project php:8.2 php index.php
The name is keyboard cost 125 โฌ
Refactoringโ
Make sure PHP Refactor Tool
is installed. Go to the list of extensions in VSCode (press CTRL-SHIFT-X) and search for PHP Refactor Tool
. Install the one of Son Tung PHAM
(the author).
Rename a symbolโ
The illustration below is showing a sample index.php
script using a class defined in Product.php
.
Everything is working fine but, yeah, functions are called getProductName
and getProductPrice
and it's quite overqualified: our object is $product
so, yes, $product->getProductName()
and $product->getProductPrice()
can be rewritten to $product->getName()
and $product->getPrice()
. The smaller the best.
So, we need to rename our functions.
By opening our Product.php
file, we can do it manually but ... don't do this because you'll need to update every single file manually using your Product
class and if you forgot just one use, your code will be broken.
The rename feature serves this objective. You just need to select a property like productName
as illustrated in the animation, press F2 and rename it.
Let's do it and see how it works:
- Open the
Product.php
file, put the cursor on the wordproductName
in line 7private string $productName = '';
, - Press F2 (or choose Rename Symbol in the Command palette (press CTRL-SHIFT-P)) and type the new name f.i.
name
(instead ofproductName
), - VSCode will display a small list at the top of the screen, just validate i.e. select
Update Getter name and Setter name
. - Do the same for
productPrice
and rename it toprice
.
The new Product.php
file is now:
<?php
namespace Cavo;
class Product
{
private string $name = '';
private float $price = 0;
public function __construct(string $name='', float $price=0)
{
$this->setName($name);
$this->setPrice($price);
}
public function getName(): string {
return $this->name;
}
public function setName(string $name): void {
$this->name = $name;
}
public function getPrice(): float {
return $this->price;
}
public function setPrice(float $price): void {
$this->price = $price;
}
}
But the very cool thing is that index.php
has been automatically updated. Open index.php
and check:
<?php
namespace Cavo;
require_once('Product.php');
$product = new Product();
$product->setName('keyboard');
$product->setPrice(125);
printf(
'The name is %s cost %d โฌ'.PHP_EOL,
$product->getName(),
$product->getPrice()
);
By running docker run -it --rm -v "${PWD}":/project -w /project php:8.2 php index.php
, it's still working.
The same thing in pictures:
Rename a classโ
For the example, reopen the Product.php
file and, on line 5, put the cursor on the word Product
. Press F2 and rename to Products
(plural form).
As we can expect, the class name has been updated but the filename too. And, once again, if you open the index.php
file, you can see that $product = new Product();
has been perfectly changed to $product = new Products();
.
Locate the $product = new Product();
line in your index.php
file. Put the cursor on the Product
word, press F2 and rename it. This work too i.e. the class will be updated too in Products.php
(since the file has been renamed too). Nice!
Renaming the file from the Explorer won't refactor the code. So, don't go to the Explorer
, click on Product.php
and rename it. This will not refactor the code. Avoid!
Extract to a new methodโ
The third very nice method is the Extract method
from PHP Refactoring.
Consider the following example (the code isn't running, it's just for the illustration). Create a new file called Pandoc.php
with this content:
<?php
namespace Avonture;
class Pandoc
{
public function download(string $filename): void
{
$filename = Sanitize::sanitizeFileName(trim($filename));
if ('' == $filename) {
throw new PandocFileNotSpecified();
}
if (!is_file($this->outputFolder . $filename)) {
throw new PandocFileNotFound($filename);
}
// Make the filename absolute
$filename = $this->outputFolder . ltrim($filename, self::DS);
$this->outputType = pathinfo($filename)['extension'];
$contentType = $this->getContentType()['type'];
header('Content-Type: ' . $contentType);
header('Content-Transfer-Encoding: ' . $this->getContentType()['encoding']);
header('Content-Disposition: attachment; filename="' . basename($filename) . '"');
header('Content-Length: ' . filesize(utf8_decode($filename)));
header('Accept-Ranges: bytes');
header('Pragma: no-cache');
header('Expires: 0');
ob_end_flush();
@readfile(utf8_decode($filename));
}
}
The download
function didn't respect the single-responsibility concept. We've made a few initializations and assertions. Can we do better? Yes, we can extract the lines concerning the browser and create a new sendToBrowser
function but instead of doing it manually, we'll use the Extract
feature.
See the animation below:
In the lines we're moving, there are variables like contentType
and filename
that are not part of the new method. These variables are still local in our first, download
method.
And, as you can see, during the creation of the new method, these two local variables have been added to the definition of the new function. Nice feature.
Other extensionsโ
- ๐ ๐ PHP Refactor also exists but the last time it was updated by the author was in 2019, four years ago, and the extract feature isn't working fine; just move along.