Skip to content
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

Setting expose-global-functions to false breaks internal function calls #960

Closed
andrewjmead opened this issue Mar 27, 2024 · 7 comments
Closed
Milestone

Comments

@andrewjmead
Copy link

Bug report

Question Answer
PHP-Scoper version ^0.18.11
PHP version 8.2.6
Platform with version MacOS
Github Repo Minimal example repository

Thanks for building PHP Scoper. It's been a huge help over the last two years.

I'm running into some unexpected behavior when using PHP Scoper with illuminate/support and other similar packages. After scoping with expose-global-functions set to false, I'm getting various errors about undefined functions.

To set the stage, illuminate/support comes with helpers.php which contains some global function definitions. This are used in a few different places, but here's an example where get_data is used on line 50 of Fluent.php.

In the example repo, I then make use of Fluent as seen below. Keep in mind this was just to demo the issue. The actual issue in the project I'm working on comes up after using illuminate/database to build some queries.

$f = new \Illuminate\Support\Fluent([
    'name' => 'Andrew',
    'is_verified' => true
]);

var_dump($f->get('name')); // "Andrew"

The code above actually works just fine with PHP Scoper, but only with expose-global-function set to true. If I set it to false, I get the following error:

PHP Fatal error:  Uncaught Error: Call to undefined function DEMO\Illuminate\Support\data_get() in /Users/andrewmead/Projects/scoper/build/vendor/illuminate/support/Fluent.php:47
Stack trace:
#0 /Users/andrewmead/Projects/scoper/build/index.php(8): DEMO\Illuminate\Support\Fluent->get('name')
#1 {main}
  thrown in /Users/andrewmead/Projects/scoper/build/vendor/illuminate/support/Fluent.php on line 47

Looking at the scoped code, I saw that things were not lining up. In helpers.php the scope was added correctly (expandable below). In Fluent.php it was still calling data_get when it really needs to call \DEMO\data_get.

helpers.php after scoping
<?php

namespace DEMO;

use DEMO\Illuminate\Contracts\Support\DeferringDisplayableValue;
use DEMO\Illuminate\Contracts\Support\Htmlable;
use DEMO\Illuminate\Support\Arr;
use DEMO\Illuminate\Support\Env;
use DEMO\Illuminate\Support\HigherOrderTapProxy;
use DEMO\Illuminate\Support\Once;
use DEMO\Illuminate\Support\Onceable;
use DEMO\Illuminate\Support\Optional;
use DEMO\Illuminate\Support\Sleep;
use DEMO\Illuminate\Support\Str;
if (!\function_exists('DEMO\\append_config')) {
   /**
    * Assign high numeric IDs to a config item to force appending.
    *
    * @param  array  $array
    * @return array
    */
   function append_config(array $array)
   {
       $start = 9999;
       foreach ($array as $key => $value) {
           if (\is_numeric($key)) {
               $start++;
               $array[$start] = Arr::pull($array, $key);
           }
       }
       return $array;
   }
}
if (!\function_exists('DEMO\\blank')) {
   /**
    * Determine if the given value is "blank".
    *
    * @param  mixed  $value
    * @return bool
    */
   function blank($value)
   {
       if (\is_null($value)) {
           return \true;
       }
       if (\is_string($value)) {
           return \trim($value) === '';
       }
       if (\is_numeric($value) || \is_bool($value)) {
           return \false;
       }
       if ($value instanceof \Countable) {
           return \count($value) === 0;
       }
       return empty($value);
   }
}
if (!\function_exists('DEMO\\class_basename')) {
   /**
    * Get the class "basename" of the given object / class.
    *
    * @param  string|object  $class
    * @return string
    */
   function class_basename($class)
   {
       $class = \is_object($class) ? \get_class($class) : $class;
       return \basename(\str_replace('\\', '/', $class));
   }
}
if (!\function_exists('DEMO\\class_uses_recursive')) {
   /**
    * Returns all traits used by a class, its parent classes and trait of their traits.
    *
    * @param  object|string  $class
    * @return array
    */
   function class_uses_recursive($class)
   {
       if (\is_object($class)) {
           $class = \get_class($class);
       }
       $results = [];
       foreach (\array_reverse(\class_parents($class) ?: []) + [$class => $class] as $class) {
           $results += trait_uses_recursive($class);
       }
       return \array_unique($results);
   }
}
if (!\function_exists('DEMO\\e')) {
   /**
    * Encode HTML special characters in a string.
    *
    * @param  \Illuminate\Contracts\Support\DeferringDisplayableValue|\Illuminate\Contracts\Support\Htmlable|\BackedEnum|string|null  $value
    * @param  bool  $doubleEncode
    * @return string
    */
   function e($value, $doubleEncode = \true)
   {
       if ($value instanceof DeferringDisplayableValue) {
           $value = $value->resolveDisplayableValue();
       }
       if ($value instanceof Htmlable) {
           return $value->toHtml();
       }
       if ($value instanceof \BackedEnum) {
           $value = $value->value;
       }
       return \htmlspecialchars($value ?? '', \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8', $doubleEncode);
   }
}
if (!\function_exists('DEMO\\env')) {
   /**
    * Gets the value of an environment variable.
    *
    * @param  string  $key
    * @param  mixed  $default
    * @return mixed
    */
   function env($key, $default = null)
   {
       return Env::get($key, $default);
   }
}
if (!\function_exists('DEMO\\filled')) {
   /**
    * Determine if a value is "filled".
    *
    * @param  mixed  $value
    * @return bool
    */
   function filled($value)
   {
       return !blank($value);
   }
}
if (!\function_exists('DEMO\\literal')) {
   /**
    * Return a new literal or anonymous object using named arguments.
    *
    * @return \stdClass
    */
   function literal(...$arguments)
   {
       if (\count($arguments) === 1 && \array_is_list($arguments)) {
           return $arguments[0];
       }
       return (object) $arguments;
   }
}
if (!\function_exists('DEMO\\object_get')) {
   /**
    * Get an item from an object using "dot" notation.
    *
    * @param  object  $object
    * @param  string|null  $key
    * @param  mixed  $default
    * @return mixed
    */
   function object_get($object, $key, $default = null)
   {
       if (\is_null($key) || \trim($key) === '') {
           return $object;
       }
       foreach (\explode('.', $key) as $segment) {
           if (!\is_object($object) || !isset($object->{$segment})) {
               return value($default);
           }
           $object = $object->{$segment};
       }
       return $object;
   }
}
if (!\function_exists('DEMO\\once')) {
   /**
    * Ensures a callable is only called once, and returns the result on subsequent calls.
    *
    * @template  TReturnType
    *
    * @param  callable(): TReturnType  $callback
    * @return TReturnType
    */
   function once(callable $callback)
   {
       $onceable = Onceable::tryFromTrace(\debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT, 2), $callback);
       return $onceable ? Once::instance()->value($onceable) : \call_user_func($callback);
   }
}
if (!\function_exists('DEMO\\optional')) {
   /**
    * Provide access to optional objects.
    *
    * @param  mixed  $value
    * @param  callable|null  $callback
    * @return mixed
    */
   function optional($value = null, callable $callback = null)
   {
       if (\is_null($callback)) {
           return new Optional($value);
       } elseif (!\is_null($value)) {
           return $callback($value);
       }
   }
}
if (!\function_exists('DEMO\\preg_replace_array')) {
   /**
    * Replace a given pattern with each value in the array in sequentially.
    *
    * @param  string  $pattern
    * @param  array  $replacements
    * @param  string  $subject
    * @return string
    */
   function preg_replace_array($pattern, array $replacements, $subject)
   {
       return \preg_replace_callback($pattern, function () use(&$replacements) {
           foreach ($replacements as $value) {
               return \array_shift($replacements);
           }
       }, $subject);
   }
}
if (!\function_exists('DEMO\\retry')) {
   /**
    * Retry an operation a given number of times.
    *
    * @param  int|array  $times
    * @param  callable  $callback
    * @param  int|\Closure  $sleepMilliseconds
    * @param  callable|null  $when
    * @return mixed
    *
    * @throws \Exception
    */
   function retry($times, callable $callback, $sleepMilliseconds = 0, $when = null)
   {
       $attempts = 0;
       $backoff = [];
       if (\is_array($times)) {
           $backoff = $times;
           $times = \count($times) + 1;
       }
       beginning:
       $attempts++;
       $times--;
       try {
           return $callback($attempts);
       } catch (\Exception $e) {
           if ($times < 1 || $when && !$when($e)) {
               throw $e;
           }
           $sleepMilliseconds = $backoff[$attempts - 1] ?? $sleepMilliseconds;
           if ($sleepMilliseconds) {
               Sleep::usleep(value($sleepMilliseconds, $attempts, $e) * 1000);
           }
           goto beginning;
       }
   }
}
if (!\function_exists('DEMO\\str')) {
   /**
    * Get a new stringable object from the given string.
    *
    * @param  string|null  $string
    * @return \Illuminate\Support\Stringable|mixed
    */
   function str($string = null)
   {
       if (\func_num_args() === 0) {
           return new class
           {
               public function __call($method, $parameters)
               {
                   return Str::$method(...$parameters);
               }
               public function __toString()
               {
                   return '';
               }
           };
       }
       return Str::of($string);
   }
}
if (!\function_exists('DEMO\\tap')) {
   /**
    * Call the given Closure with the given value then return the value.
    *
    * @param  mixed  $value
    * @param  callable|null  $callback
    * @return mixed
    */
   function tap($value, $callback = null)
   {
       if (\is_null($callback)) {
           return new HigherOrderTapProxy($value);
       }
       $callback($value);
       return $value;
   }
}
if (!\function_exists('DEMO\\throw_if')) {
   /**
    * Throw the given exception if the given condition is true.
    *
    * @template TException of \Throwable
    *
    * @param  mixed  $condition
    * @param  TException|class-string<TException>|string  $exception
    * @param  mixed  ...$parameters
    * @return mixed
    *
    * @throws TException
    */
   function throw_if($condition, $exception = 'RuntimeException', ...$parameters)
   {
       if ($condition) {
           if (\is_string($exception) && \class_exists($exception)) {
               $exception = new $exception(...$parameters);
           }
           throw \is_string($exception) ? new \RuntimeException($exception) : $exception;
       }
       return $condition;
   }
}
if (!\function_exists('DEMO\\throw_unless')) {
   /**
    * Throw the given exception unless the given condition is true.
    *
    * @template TException of \Throwable
    *
    * @param  mixed  $condition
    * @param  TException|class-string<TException>|string  $exception
    * @param  mixed  ...$parameters
    * @return mixed
    *
    * @throws TException
    */
   function throw_unless($condition, $exception = 'RuntimeException', ...$parameters)
   {
       throw_if(!$condition, $exception, ...$parameters);
       return $condition;
   }
}
if (!\function_exists('DEMO\\trait_uses_recursive')) {
   /**
    * Returns all traits used by a trait and its traits.
    *
    * @param  object|string  $trait
    * @return array
    */
   function trait_uses_recursive($trait)
   {
       $traits = \class_uses($trait) ?: [];
       foreach ($traits as $trait) {
           $traits += trait_uses_recursive($trait);
       }
       return $traits;
   }
}
if (!\function_exists('DEMO\\transform')) {
   /**
    * Transform the given value if it is present.
    *
    * @template TValue of mixed
    * @template TReturn of mixed
    * @template TDefault of mixed
    *
    * @param  TValue  $value
    * @param  callable(TValue): TReturn  $callback
    * @param  TDefault|callable(TValue): TDefault|null  $default
    * @return ($value is empty ? ($default is null ? null : TDefault) : TReturn)
    */
   function transform($value, callable $callback, $default = null)
   {
       if (filled($value)) {
           return $callback($value);
       }
       if (\is_callable($default)) {
           return $default($value);
       }
       return $default;
   }
}
if (!\function_exists('DEMO\\windows_os')) {
   /**
    * Determine whether the current environment is Windows based.
    *
    * @return bool
    */
   function windows_os()
   {
       return \PHP_OS_FAMILY === 'Windows';
   }
}
if (!\function_exists('DEMO\\with')) {
   /**
    * Return the given value, optionally passed through the given callback.
    *
    * @template TValue
    * @template TReturn
    *
    * @param  TValue  $value
    * @param  (callable(TValue): (TReturn))|null  $callback
    * @return ($callback is null ? TValue : TReturn)
    */
   function with($value, callable $callback = null)
   {
       return \is_null($callback) ? $value : $callback($value);
   }
}
Fluent.php after scoping
<?php

namespace DEMO\Illuminate\Support;

use ArrayAccess;
use DEMO\Illuminate\Contracts\Support\Arrayable;
use DEMO\Illuminate\Contracts\Support\Jsonable;
use JsonSerializable;
/**
* @template TKey of array-key
* @template TValue
*
* @implements \Illuminate\Contracts\Support\Arrayable<TKey, TValue>
* @implements \ArrayAccess<TKey, TValue>
*/
class Fluent implements Arrayable, ArrayAccess, Jsonable, JsonSerializable
{
   /**
    * All of the attributes set on the fluent instance.
    *
    * @var array<TKey, TValue>
    */
   protected $attributes = [];
   /**
    * Create a new fluent instance.
    *
    * @param  iterable<TKey, TValue>  $attributes
    * @return void
    */
   public function __construct($attributes = [])
   {
       foreach ($attributes as $key => $value) {
           $this->attributes[$key] = $value;
       }
   }
   /**
    * Get an attribute from the fluent instance using "dot" notation.
    *
    * @template TGetDefault
    *
    * @param  TKey  $key
    * @param  TGetDefault|(\Closure(): TGetDefault)  $default
    * @return TValue|TGetDefault
    */
   public function get($key, $default = null)
   {
       return data_get($this->attributes, $key, $default);
   }
   /**
    * Get an attribute from the fluent instance.
    *
    * @param  string  $key
    * @param  mixed  $default
    * @return mixed
    */
   public function value($key, $default = null)
   {
       if (\array_key_exists($key, $this->attributes)) {
           return $this->attributes[$key];
       }
       return value($default);
   }
   /**
    * Get the value of the given key as a new Fluent instance.
    *
    * @param  string  $key
    * @param  mixed  $default
    * @return static
    */
   public function scope($key, $default = null)
   {
       return new static((array) $this->get($key, $default));
   }
   /**
    * Get the attributes from the fluent instance.
    *
    * @return array<TKey, TValue>
    */
   public function getAttributes()
   {
       return $this->attributes;
   }
   /**
    * Convert the fluent instance to an array.
    *
    * @return array<TKey, TValue>
    */
   public function toArray()
   {
       return $this->attributes;
   }
   /**
    * Convert the fluent instance to a Collection.
    *
    * @param  string|null  $key
    * @return \Illuminate\Support\Collection
    */
   public function collect($key = null)
   {
       return new Collection($this->get($key));
   }
   /**
    * Convert the object into something JSON serializable.
    *
    * @return array<TKey, TValue>
    */
   public function jsonSerialize() : array
   {
       return $this->toArray();
   }
   /**
    * Convert the fluent instance to JSON.
    *
    * @param  int  $options
    * @return string
    */
   public function toJson($options = 0)
   {
       return \json_encode($this->jsonSerialize(), $options);
   }
   /**
    * Determine if the given offset exists.
    *
    * @param  TKey  $offset
    * @return bool
    */
   public function offsetExists($offset) : bool
   {
       return isset($this->attributes[$offset]);
   }
   /**
    * Get the value for a given offset.
    *
    * @param  TKey  $offset
    * @return TValue|null
    */
   public function offsetGet($offset) : mixed
   {
       return $this->value($offset);
   }
   /**
    * Set the value at the given offset.
    *
    * @param  TKey  $offset
    * @param  TValue  $value
    * @return void
    */
   public function offsetSet($offset, $value) : void
   {
       $this->attributes[$offset] = $value;
   }
   /**
    * Unset the value at the given offset.
    *
    * @param  TKey  $offset
    * @return void
    */
   public function offsetUnset($offset) : void
   {
       unset($this->attributes[$offset]);
   }
   /**
    * Handle dynamic calls to the fluent instance to set attributes.
    *
    * @param  TKey  $method
    * @param  array{0: ?TValue}  $parameters
    * @return $this
    */
   public function __call($method, $parameters)
   {
       $this->attributes[$method] = \count($parameters) > 0 ? \reset($parameters) : \true;
       return $this;
   }
   /**
    * Dynamically retrieve the value of an attribute.
    *
    * @param  TKey  $key
    * @return TValue|null
    */
   public function __get($key)
   {
       return $this->value($key);
   }
   /**
    * Dynamically set the value of an attribute.
    *
    * @param  TKey  $key
    * @param  TValue  $value
    * @return void
    */
   public function __set($key, $value)
   {
       $this->offsetSet($key, $value);
   }
   /**
    * Dynamically check if an attribute is set.
    *
    * @param  TKey  $key
    * @return bool
    */
   public function __isset($key)
   {
       return $this->offsetExists($key);
   }
   /**
    * Dynamically unset an attribute.
    *
    * @param  TKey  $key
    * @return void
    */
   public function __unset($key)
   {
       $this->offsetUnset($key);
   }
}

Is this the expected behavior here? It's worth noting that if I manually change the calls in scoped code, it works perfectly fine. It's just not adding the scope to those global function calls when scoping them.

Thanks for taking the time to look into this one. I hope I provided enough details about the issue.

Here's a zip of the build after scoping the project with expose-global-functions set to false:

build.zip

scoper.inc.php
<?php

declare(strict_types=1);

use Isolated\Symfony\Component\Finder\Finder;

// You can do your own things here, e.g. collecting symbols to expose dynamically
// or files to exclude.
// However beware that this file is executed by PHP-Scoper, hence if you are using
// the PHAR it will be loaded by the PHAR. So it is highly recommended to avoid
// to auto-load any code here: it can result in a conflict or even corrupt
// the PHP-Scoper analysis.

// Example of collecting files to include in the scoped build but to not scope
// leveraging the isolated finder.
$excludedFiles = array_map(
   static fn (SplFileInfo $fileInfo) => $fileInfo->getPathName(),
   iterator_to_array(
       Finder::create()->files()->in(__DIR__),
       false,
   ),
);

return [
   // The prefix configuration. If a non-null value is used, a random prefix
   // will be generated instead.
   //
   // For more see: https://github.com/humbug/php-scoper/blob/master/docs/configuration.md#prefix
   'prefix' => 'DEMO',

   // The base output directory for the prefixed files.
   // This will be overridden by the 'output-dir' command line option if present.
   'output-dir' => null,

   // By default when running php-scoper add-prefix, it will prefix all relevant code found in the current working
   // directory. You can however define which files should be scoped by defining a collection of Finders in the
   // following configuration key.
   //
   // This configuration entry is completely ignored when using Box.
   //
   // For more see: https://github.com/humbug/php-scoper/blob/master/docs/configuration.md#finders-and-paths
   'finders' => [
       Finder::create()
           ->files()
           ->ignoreVCS(true)
           ->notName('/LICENSE|.*\\.md|.*\\.dist|Makefile|composer\\.json|composer\\.lock/')
           ->exclude([
               'doc',
               'test',
               'test_old',
               'tests',
               'Tests',
               'vendor-bin',
           ])
           ->in('vendor'),
       Finder::create()->append([
           'composer.json',
           'index.php'
       ]),
   ],

   // List of excluded files, i.e. files for which the content will be left untouched.
   // Paths are relative to the configuration file unless if they are already absolute
   //
   // For more see: https://github.com/humbug/php-scoper/blob/master/docs/configuration.md#patchers
   'exclude-files' => [
       // 'src/an-excluded-file.php',
       // ...$excludedFiles,
   ],

   // When scoping PHP files, there will be scenarios where some of the code being scoped indirectly references the
   // original namespace. These will include, for example, strings or string manipulations. PHP-Scoper has limited
   // support for prefixing such strings. To circumvent that, you can define patchers to manipulate the file to your
   // heart contents.
   //
   // For more see: https://github.com/humbug/php-scoper/blob/master/docs/configuration.md#patchers
   'patchers' => [
       static function (string $filePath, string $prefix, string $contents): string {
           // Change the contents here.

           return $contents;
       },
   ],

   // List of symbols to consider internal i.e. to leave untouched.
   //
   // For more information see: https://github.com/humbug/php-scoper/blob/master/docs/configuration.md#excluded-symbols
   'exclude-namespaces' => [
       // 'Acme\Foo'                     // The Acme\Foo namespace (and sub-namespaces)
       // '~^PHPUnit\\\\Framework$~',    // The whole namespace PHPUnit\Framework (but not sub-namespaces)
       // '~^$~',                        // The root namespace only
       // '',                            // Any namespace
   ],
   'exclude-classes' => [
       // 'ReflectionClassConstant',
   ],
   'exclude-functions' => [
       // 'mb_str_split',
   ],
   'exclude-constants' => [
       // 'STDIN',
   ],

   // List of symbols to expose.
   //
   // For more information see: https://github.com/humbug/php-scoper/blob/master/docs/configuration.md#exposed-symbols
   'expose-global-constants' => true,
   'expose-global-classes' => true,
   'expose-global-functions' => true,
   'expose-namespaces' => [
       // 'Acme\Foo'                     // The Acme\Foo namespace (and sub-namespaces)
       // '~^PHPUnit\\\\Framework$~',    // The whole namespace PHPUnit\Framework (but not sub-namespaces)
       // '~^$~',                        // The root namespace only
       // '',                            // Any namespace
   ],
   'expose-classes' => [],
   'expose-functions' => [],
   'expose-constants' => [],
];
Output
$ vendor/bin/php-scoper add-prefix --force && composer dump-autoload --working-dir build --classmap-authoritative 


   ____  __  ______     _____
  / __ \/ / / / __ \   / ___/_________  ____  ___  _____
 / /_/ / /_/ / /_/ /   \__ \/ ___/ __ \/ __ \/ _ \/ ___/
/ ____/ __  / ____/   ___/ / /__/ /_/ / /_/ /  __/ /
/_/   /_/ /_/_/       /____/\___/\____/ .___/\___/_/
                                    /_/

PhpScoper version 0.18.11@d4df72a

1846/1846 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

                                                                                                                       
[OK] Successfully prefixed 1846 files.                                                                                 
                                                                                                                       

// Memory usage: 23.82MB (peak: 34.59MB), time: 4.05s                                                                  

Generating optimized autoload files (authoritative)
Generated optimized autoload files (authoritative) containing 746 classes
@andrewjmead andrewjmead changed the title Setting expose-global-functions to false breaks internal function calls to functions Setting expose-global-functions to false breaks internal function calls Mar 27, 2024
@theofidry
Copy link
Member

Is this the expected behavior here? It's worth noting that if I manually change the calls in scoped code, it works perfectly fine. It's just not adding the scope to those global function calls when scoping them.

I thought this was documented in https://github.com/humbug/php-scoper/blob/main/docs/limitations.md but apparently not. So I'll explain a bit more in details why it happens.

The root of the issue, is not so much whether expose-global-function or the usage of global functions, but the fact that they are used relying on PHP function fallback autoloading mechanism. Concretely, in your Fluent class, when you call get_data(), since it is not a fully-qualified call (\get_data()) and since there is no function import statement (use function get_data;), PHP will see the function get_data and first try to load Illuminate\Support\Fluent\get_data, and if does not exists, fallback on get_data.

When PHP-Scoper comes in and scopes the Fluent.php file, it just sees a function get_data. Like PHP, it can't figure out if it's Illuminate\Support\Fluent\get_data or get_data. However the declaration of get_data does get changed (because expose-global-functions=false), resulting in the global function get_data no longer exists, breaking your code.

So at the moment, it is unfortunately expected.


THAT SAID. I'm wondering if there wouldn't be a way to handle it, for example PHP-Scoper could keep track of those ambiguous function calls, and if expose-global-functions=false. It could do something along those lines in the scoper-autoload.php:

// for ambiguous `get_data` which could be `Illuminate\Support\Fluent\get_data` or `get_data`
// only when `expose-global-functions=false`
namespace Prefix\Illuminate\Support\Fluent;

if (!function_exists('Prefix\Illuminate\Support\Fluent\get_data') {
  function get_data() { return \Prefix\get_data(...func_get_args()); }
}

This way, if get_data was Illuminate\Support\Fluent\get_data, then Prefix\Illuminate\Support\Fluent\get_data exists and should already be loaded, so nothing will happen. But, if it does not, then we declare Prefix\Illuminate\Support\Fluent\get_data to alias Prefix\get_data and it should no be fine.

Would need to try out but on paper it should work

@andrewjmead
Copy link
Author

The distinction between \func() and func() makes perfect sense. Thanks for pointing that out.

I'd love to see support for this, but I just just wanted to make sure I wasn't doing anything wrong. I was able to use a regex patch for the moment, but might move on to something like a custom php-parser script as a more long-term fix for this.

I appreciate the detailed reply.

@theofidry theofidry added this to the 1.0.0 milestone Apr 11, 2024
theofidry added a commit to theofidry/php-scoper that referenced this issue Apr 12, 2024
@theofidry
Copy link
Member

THAT SAID. I'm wondering if there wouldn't be a way to handle it, for example PHP-Scoper could keep track of those ambiguous function calls, and if expose-global-functions=false. It could do something along those lines in the scoper-autoload.php:

After reviewing this, it does not make any sense. The reason being, this why expose-global-functions=true by default and it has the side-effect of exposing methods. Being able to disable this is a feature and it is precisely what expose-global-functions=false is about. Sorry for the brain fart 😅

@andrewjmead
Copy link
Author

Thanks for following up. I appreciate you taking another look. I was able to get things working and stable with a patch.

@theofidry
Copy link
Member

Is it necessary to use a patch? Was using expose-global-functions=true not acceptable?

@andrewjmead
Copy link
Author

Using expose-global-functions=true worked as designed, but the issue was with other WordPress plugins also using illuminate/database.

As an example, my scoped plugin calls the global collect function provided by illuminate/support. The catch was that it would only call my version of collect if no other plugins were in conflict. If another plugin had it defined, their version would get called. I was running into issues with other plugins using much newer or older versions of illuminate/databsae where the APIs were no longer compatible.

@theofidry
Copy link
Member

I see, indeed it's a case for patchers then :/ Unless this could be patched in Laravel but I doubt think use statements for functions is a flavor liked by the maintainers

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

Successfully merging a pull request may close this issue.

2 participants