How to Update to PHP 7.4 Typed Property Without BC Break with Rector
In Rector 0.12.9, `TypedPropertyRector` is not configurable, it will only change:
– private property
– protected property on final class without extends
In Rector 0.12.16, `TypedPropertyRector` has configurable to allow change protected and public modifier as well as far when possible with new configurable:
$services->set(TypedPropertyRector::class) ->configure([ TypedPropertyRector::INLINE_PUBLIC => true, ]);
This Article is valid for Rector <= 0.12.8
Typed Property is one of the PHP 7.4 feature that allow to write that previously like this:
namespace Lib; class SomeClass { /** @var int */ public $a; /** @var string */ protected $b; /** @var bool */ private $c; }
to this:
namespace Lib; class SomeClass { public int $a; protected string $b; private bool $c; }
If you follow Semver for versioning, and you don’t want to update to major change, eg: version 1 to version 2, changing this will make Break Backward Compatibility, for example:
namespace Lib; class SomeClass { protected string $b; }
has child in application consumer:
namespace App; use Lib\SomeClass; class AChild extends SomeClass { protected $b; }
will result a fatal error:
Fatal error: Type of AChild::$b must be string (as in class SomeClass)
see https://3v4l.org/X9Yvd . To avoid that, you should only change to private modifier only, so, the change will only to private property:
namespace Lib; class SomeClass { /** @var int */ public $a; /** @var string */ protected $b; - /** @var bool */ - private $c; + private bool $c; }
Want to automate that? You can use Rector for it. First, let say, we have a re-usable package that can be consumed in our applications, with the following package structure:
lib ├── composer.json ├── composer.lock ├── src │ └── SomeClass.php
with composer.json config like this:
{ "require": { "php": "^7.4" }, "autoload": { "psr-4": { "Lib\\": "src/" } } }
Your package will be hosted in packagist or your own server.
Now, what you need is require the rector as dev dependency by go to lib
directory:
cd lib/ composer require --dev rector/rector
Rector has rule named TypedPropertyRector
, that part of SetList::PHP_74
constant.
It default will update all modifiers:
- public
- protected
- private
If you are using on projects that not re-usable project, you can just use SetList::PHP_74
constant as is.
For our use case, you can override it by configure it to only apply to private property only.
You can create a rector.php
configuration inside the root of lib
directory as follow:
<?php declare(strict_types=1); use Rector\Core\Configuration\Option; use Rector\Core\ValueObject\PhpVersion; use Rector\Php74\Rector\Property\TypedPropertyRector; use Rector\Set\ValueObject\SetList; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { $parameters = $containerConfigurator->parameters(); $parameters->set(Option::PATHS, [ __DIR__ . '/src' ]); $parameters->set(Option::PHP_VERSION_FEATURES, PhpVersion::PHP_74); // import php 7.4 set list for php 7.4 features $containerConfigurator->import(SetList::PHP_74); // set Typed Property only for private property $services = $containerConfigurator->services(); $services->set(TypedPropertyRector::class) ->call('configure', [[ TypedPropertyRector::PRIVATE_PROPERTY_ONLY => true, ]]); };
Above, we import php 7.4 set list, with configured TypedPropertyRector
for update to typed property to only change private property only.
Now, let’s run rector to see the diff and verify:
cd lib vendor/bin/rector --dry-run
Everything seems correct! Let’s apply the change:
cd lib vendor/bin/rector
Now, you have typed property in your code!
That’s it!
Practical Regex 2: Using Named Backreference in Positive Lookahead
When we have the following namespaced classname value:
App\Domain\Foo\FooRepository
and our task is to get “Foo” value as Domain group with the criteria:
- must have
App\Domain\
prefix - must have
\$Domain
name + “Repository” suffix which$Domain
must match previous sub-namespace, on this case, “Foo”
we can use the following regex with Positive Lookbehind
to filter prefix and Positive Lookahead
to filter suffix:
(?<=App\\Domain\\)(?<Domain>[A-Z][a-z]{1,})(?=\\\1Repository)
Above, we find $Domain
with App\Domain\
before it with Positive Lookbehind
, and $Domain
Repository after it with Positive Lookahead
.
We are using backreference with \1
to match the exact same text of the first capturing group. If you code in PHP, you can do like this:
$pattern = '#(?<=App\\\\Domain\\\\)(?<Domain>[A-Z][a-z]{1,})(?=\\\\\1Repository)#'; $className = 'App\Domain\Foo\FooRepository'; preg_match($pattern, $className, $matches); if ($matches !== []) { echo $matches['Domain']; }
What if the code is getting more complex, like in my previous post for named capturing group, you need to remember the numbered index! To handle it, you can use named backreference for it, so the regex will be:
(?<=App\\Domain\\)(?<Domain>[A-Z][a-z]{1,})(?=\\\k<Domain>Repository)
Now, you are exactly know what backrefence reference to. If you code in PHP, you can do like this:
$pattern = '#(?<=App\\\\Domain\\\\)(?<Domain>[A-Z][a-z]{1,})(?=\\\\\k<Domain>Repository)#'; $className = 'App\Domain\Foo\FooRepository'; preg_match($pattern, $className, $matches); if ($matches !== []) { echo $matches['Domain']; }
That’s it 😉
Practical Regex 1: Using Named Capturing Groups
Still using numbered group of capture value in Regex? You may forget, or the index changed if the requirement changed. For example, you want to get a csrf value from a form field with the following regex example:
name="csrf" value="(.{32})"
For input field csrf with 32 chars value “4X0ZfDKr71KHCec7SOkoJ5onq1PTCN3v”, you want to get the value, you will need to get index 1 for it with PHP:
<?php $pattern = '#name="csrf" value="(.{32})"#'; $content = <<<'HTML_CONTENT' <form> <input type="hidden" name="csrf" value="4X0ZfDKr71KHCec7SOkoJ5onq1PTCN3v" /> <input type="submit" /> </form> HTML_CONTENT; preg_match($pattern, $content, $matches); if ($matches !== []) { echo $matches[1]; }
To handle the possible forgotten index or changed index that can create a bug, you can use named capturing groups, so you can change to:
name="csrf" value="(?<csrf_value>.{32})"
Now, you can get it easily:
<?php $pattern = '#name="csrf" value="(?<csrf_value>.{32})"#'; $content = <<<'HTML_CONTENT' <form> <input type="hidden" name="csrf" value="4X0ZfDKr71KHCec7SOkoJ5onq1PTCN3v" /> <input type="submit" /> </form> HTML_CONTENT; preg_match($pattern, $content, $matches); if ($matches !== []) { echo $matches['csrf_value']; }
That’s it 😉
leave a comment