Skip to content

feat: Rector rules to help migrating away from proxy #941

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 12 commits into
base: feat/proxy-php84
Choose a base branch
from

Conversation

nikophil
Copy link
Member

@nikophil nikophil commented Jun 15, 2025

see #899 (comment)

I'm intentionally not supporting all "fluent" syntax, because they are way to complex to handle with Rector

ie: something like proxy($object)->_save(); will be left untouched

This is a "best effort" stuff, I think everything cannot be removed automatically, but this should make the heavy lifting

(I still need to test it on a big project)

@nikophil nikophil changed the title rector/remove proxy feat: add Rector set to help migrating away from proxy mechanism Jun 15, 2025
@nikophil nikophil changed the base branch from 2.x to feat/proxy-php84 June 15, 2025 14:04
@nikophil nikophil force-pushed the rector/remove-proxy branch 3 times, most recently from c2eb0fd to ee7a8b2 Compare June 15, 2025 14:17
@nikophil nikophil changed the title feat: add Rector set to help migrating away from proxy mechanism feat: Rector rules to help migrating away from proxy Jun 15, 2025
@nikophil nikophil force-pushed the rector/remove-proxy branch from ee7a8b2 to 85ad1d4 Compare June 15, 2025 16:50
@nikophil nikophil marked this pull request as ready for review June 15, 2025 21:00
@nikophil nikophil force-pushed the rector/remove-proxy branch from fcea7d0 to 3ab1e1f Compare June 15, 2025 21:07
@nikophil nikophil requested a review from kbond June 15, 2025 21:08
@nikophil nikophil force-pushed the rector/remove-proxy branch from 3ab1e1f to 8f02e9a Compare June 15, 2025 21:15
@nikophil nikophil removed the request for review from kbond June 16, 2025 08:51
@nikophil nikophil marked this pull request as draft June 16, 2025 08:52
@nikophil nikophil force-pushed the rector/remove-proxy branch from 77b96e2 to 878421d Compare June 19, 2025 20:14
/**
* @param MethodCall $node
*/
public function refactor(Node $node) : ?Node

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's this one, right?

I'll make notes as it comes to my mind:

  • I'd avoid configuration and use values here instead. The fluent call has to be converted fully, to its split functoins.
  • If you want to return multiple stmts = standalone lines with function calls, you need to hook into and Expression node. That way we're working with lines, and can return multiple lines.
  • The Expression node must contain $node->expr of MethodCall instance, then you create functions and return them all.

First notes to kick off.

Copy link
Member Author

@nikophil nikophil Jun 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First, thanks for your help :)

To give you some context, the Proxy mechanism in Foundry is clunky, and PHP 8.4 lazy objects gives us a way to drastically improve this. So I want to remove everything dealing with proxies. We have functions helper that can replace each method from Proxy class.

I think I'll create a dedicated rector for this case: the other fluent methods will be replaced with functions that actually return the object, so, we could mimic the fluent syntax with them.
eg: $object->_save()->_save() will become save(save($object)).

Sadly, the method Proxy::_set() is the more complex one to replace, and is one of the most used in the wild. If it wasn't the case, I would have let the user deal with edge cases by themselves: this Rector set is a "best effort" thing, they will have to fix stuff manually anyway... More over, not dealing with this specific method will lead to broken code after the Rector set has been run.

If you want to return multiple stmts = standalone lines with function calls, you need to hook into and Expression node. That way we're working with lines, and can return multiple lines.

yes, that's what I finally understood 😅 but dealing with Expression is a lot more challenging.

if the method I want to remove is in the middle of the expression (eg: $proxy->_save()->_set()->_save() or $someService->doSomething($proxy->_set()->save())), I don't know how to deal with it. Is there some special "expression traverser" before I dive into a solution using recursion?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the code example you gave is pretty clear, thanks

How would you handle the case where you need to use a new variable, I know this pattern is widly used as well:

- SomeFactory::createOne()
-    ->_set('prop1', 'value1')
-    ->_save()
-    ->_set('prop2', 'value2')
-    ->_save()
-    ->_real();

+ $object = SomeFactory::createOne();
+ Zenstruck\Foundry\set($object, 'prop1', 'value1');
+ $object = $object->save();
+ Zenstruck\Foundry\set($object, 'prop2', 'value2');
+ $object->save()->_real();

is there some helper to generate unused var names?

Copy link

@TomasVotruba TomasVotruba Jun 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you handle the case where you need to use a new variable,

If there is convention of naming factories by their entity, I'd simply use the prefix from SomeFactorySome :)


if the method I want to remove is in the middle of the expression (eg: $proxy->_save()->_set()->_save() or $someService->doSomething($proxy->_set()->save())),

Removing node in the middle of the chain is mental hack. Nested calls work in inversed order.

So:

$some->first()->second()->third();

Is third(), second(), then first() in the AST tree. This might help:

https://getrector.com/ast/008d8f973cd69fa967a1aaaf5c8c04459fd3ea34

Removing a node somewhere in the middle is done like this:

// here $node->var, is `MethodCall` of second()`

// to remove if, just assign it the node above (third())
$node->var  = $node;

/**
* @param MethodCall $node
*/
public function refactor(Node $node) : ?Node

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breif example:

if (! $node->expr instanceof MethodCall) {
    return null;
}

$stmtsToReturn = [];

$currentMethodCall = $node->expr;
do { ... 
    // create func  call here
    $funcCall = new FuncCall(...);
    $stmtsToReturn[] = new Expression($funcCall);

    // jump to next nested method call
    $currentMethodCall = $currentMethodCall->var;	 
} while ($currentMethodCall instanceof MethodCall);



if ($stmtsToReturn === []) {
    return null;
}

return $stmtsToReturn;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants