Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: Happyr/service-mocking
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 0.1.2
Choose a base ref
...
head repository: Happyr/service-mocking
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref

Commits on Dec 25, 2020

  1. Readme updates (#9)

    * Readme updates
    
    * Added internal annotations
    Nyholm authored Dec 25, 2020

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    69b1f33 View commit details

Commits on Dec 26, 2020

  1. Reload objects (#11)

    * Reload objects
    
    * Added some more tests
    
    * Fixed tests
    
    * cs
    
    * Fixes
    
    * cs
    Nyholm authored Dec 26, 2020
    Copy the full SHA
    3471573 View commit details
  2. Copy the full SHA
    cdac49a View commit details

Commits on Jan 16, 2021

  1. Make sure mocked service is not lazy (#14)

    This will fix #13
    Nyholm authored Jan 16, 2021
    Copy the full SHA
    8481d08 View commit details

Commits on Apr 23, 2021

  1. Copy the full SHA
    58d222d View commit details

Commits on Jul 11, 2021

  1. Add limitation (#16)

    * Add limitation
    
    * Update README.md
    
    Co-authored-by: Tobias Nyholm <tobias.nyholm@gmail.com>
    OskarStark and Nyholm authored Jul 11, 2021
    Copy the full SHA
    5a6519a View commit details
  2. Make CI green (#17)

    * Make CI green
    
    * Try PHP74
    
    * Different version of psalm
    
    * fix
    Nyholm authored Jul 11, 2021
    Copy the full SHA
    f564b59 View commit details

Commits on Aug 20, 2021

  1. Add PHP config example (#19)

    * Add PHP config example
    
    * Syntax
    Nyholm authored Aug 20, 2021
    Copy the full SHA
    0694254 View commit details
  2. Update syntax in readme (#20)

    Nyholm authored Aug 20, 2021
    Copy the full SHA
    2b41327 View commit details
  3. Add support for services created with factories (#21)

    * Add support for services with factories
    
    * Added tests
    
    * cs
    
    * Fixed tests
    
    * PHP 7.2 fix
    Nyholm authored Aug 20, 2021
    Copy the full SHA
    8da1c44 View commit details
  4. Add support for Symfony 6 (#18)

    * Add support for Symfony 6
    
    * Make sure SF6 is installable
    
    * Include Symfony 6 differently
    
    * Syntax fix
    Nyholm authored Aug 20, 2021
    Copy the full SHA
    bf474ca View commit details
  5. Release 0.2.0 (#22)

    Nyholm authored Aug 20, 2021
    Copy the full SHA
    97e0b64 View commit details
  6. Added warning about indirect method calls (#23)

    * Added warning about indirect method calls
    
    * typo
    Nyholm authored Aug 20, 2021
    Copy the full SHA
    a46b9ef View commit details

Commits on Apr 3, 2022

  1. Make CI green (#26)

    * CI fixes
    
    * Allow dev-master of nyholm/symfony-bundle-test
    
    * cs
    
    * Reduce number of SF versions
    
    * Adapt to version 2 of test lib
    
    * Fixes
    
    * minor
    
    * Updated baselien
    Nyholm authored Apr 3, 2022
    Copy the full SHA
    4366f0e View commit details
  2. Make mocked services non-lazy (#25)

    This will avoid problems like
    > A method by name setProxyInitializer already exists in this class.
    Nyholm authored Apr 3, 2022
    Copy the full SHA
    707be07 View commit details
  3. Release 0.3.0

    Nyholm committed Apr 3, 2022
    Copy the full SHA
    b2f25c5 View commit details

Commits on Apr 5, 2022

  1. Copy the full SHA
    b7151db View commit details
  2. typo

    Nyholm committed Apr 5, 2022
    Copy the full SHA
    cb30172 View commit details

Commits on Jul 7, 2022

  1. Add SkipDestructor (#28)

    * Add failing test
    
    * Add SkipDestructor
    IonBazan authored Jul 7, 2022
    Copy the full SHA
    fdb15e4 View commit details
  2. Release 0.3.1 (#29)

    Nyholm authored Jul 7, 2022
    Copy the full SHA
    b1857bc View commit details

Commits on Oct 8, 2024

  1. Ci update (#32)

    * improve CI
    
    * update CI
    
    * add void
    
    * drop php 7.2
    Nyholm authored Oct 8, 2024
    Copy the full SHA
    0bf9a22 View commit details
  2. feat: Symfony 7 support (#31)

    * feat: Symfony 7 support
    
    * chore: trigger ci
    
    * fix
    
    * support test-bundle 3.0
    
    * fix psalm
    
    ---------
    
    Co-authored-by: Tobias Nyholm <tobias.nyholm@gmail.com>
    kbond and Nyholm authored Oct 8, 2024
    Copy the full SHA
    ccd0e0f View commit details
  3. Release 1.0.0 (#33)

    * prepare for release 1.0
    
    * updatae readme
    Nyholm authored Oct 8, 2024
    Copy the full SHA
    f6a7f50 View commit details
14 changes: 7 additions & 7 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.editorconfig export-ignore
.gitattributes export-ignore
/.github/ export-ignore
.gitignore export-ignore
/.php_cs export-ignore
/phpunit.xml.dist export-ignore
/tests/ export-ignore
.editorconfig export-ignore
.gitattributes export-ignore
/.github/ export-ignore
.gitignore export-ignore
/.php-cs-fixer.php export-ignore
/phpunit.xml.dist export-ignore
/tests/ export-ignore
15 changes: 9 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
name: Tests
on: [push, pull_request]
on: [pull_request]

jobs:
build:
name: Build
phpunit:
name: PHPUnit
runs-on: ubuntu-latest
strategy:
max-parallel: 10
matrix:
php: [ '7.2', '7.3', '7.4', '8.0']
sf_version: [ '4.4.*', '5.2.*' ]
php: [ '8.1', '8.2', '8.3']
sf_version: [ '5.4.*', '6.4.*', '7.*' ]
exclude:
- php: 8.1
sf_version: '7.*'

steps:
- name: Set up PHP
@@ -38,7 +41,7 @@ jobs:
- name: Set up PHP
uses: shivammathur/setup-php@2.5.0
with:
php-version: 7.3
php-version: 8.1
coverage: none

- name: Checkout code
65 changes: 54 additions & 11 deletions .github/workflows/static.yml
Original file line number Diff line number Diff line change
@@ -1,30 +1,45 @@
on: [push, pull_request]
on: [pull_request]
name: Static analysis

jobs:
phpstan:
name: PHPStan
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: PHPStan
uses: docker://oskarstark/phpstan-ga
env:
REQUIRE_DEV: true
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
args: analyze --no-progress
php-version: 8.3
coverage: none
tools: phpstan:1.12, cs2pr

- name: Download dependencies
uses: ramsey/composer-install@v1

- name: PHPStan
run: phpstan analyze --no-progress --error-format=checkstyle | cs2pr

php-cs-fixer:
name: PHP-CS-Fixer
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: PHP-CS-Fixer
uses: docker://oskarstark/php-cs-fixer-ga
- name: Checkout code
uses: actions/checkout@v2

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
args: --dry-run --diff-format udiff
php-version: 8.3
coverage: none
tools: php-cs-fixer:3.64, cs2pr

- name: PHP-CS-Fixer
run: php-cs-fixer fix --dry-run --format=checkstyle | cs2pr

psalm:
name: Psalm
@@ -33,5 +48,33 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
coverage: none
tools: vimeo/psalm:5.26

- name: Download dependencies
uses: ramsey/composer-install@v1

- name: Psalm
uses: docker://vimeo/psalm-github-actions
run: psalm --no-progress --output-format=github

composer-normalize:
name: Composer Normalize
runs-on: ubuntu-latest

steps:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
coverage: none
tools: composer-normalize

- name: Checkout code
uses: actions/checkout@v2

- name: Normalize
run: composer-normalize --dry-run
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/.php_cs.cache
/.php-cs-fixer.cache
/.phpunit.result.cache
/composer.lock
/phpunit.xml
4 changes: 2 additions & 2 deletions .php_cs → .php-cs-fixer.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<?php

$finder = PhpCsFixer\Finder::create()
$finder = (new PhpCsFixer\Finder())
->in(__DIR__.'/src')
->in(__DIR__.'/tests')
;

return PhpCsFixer\Config::create()
return (new PhpCsFixer\Config())
->setRules([
'@Symfony' => true,
])
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -2,6 +2,45 @@

The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release.

## 1.0.0

### Removed

- Removed support for < PHP 8.1

### Added

- Support for Symfony 7

## 0.3.1

### Fixed

- Added support for mock classes with destructor

## 0.3.0

### Fixed

- Make services non-lazy to avoid "A method by name setProxyInitializer already exists in this class."

## 0.2.0

### Added

- Support for services created with factories
- Support for Symfony 6

### Changed

- All proxied services are lazy

## 0.1.3

### Changed

- The real object is updated whenever a proxy is initialized.

## 0.1.2

### Changed
66 changes: 59 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -36,7 +36,31 @@ return [
## Configure services

You need to tell the bundle what services you want to mock. That could be done with
the service tag `happyr_service_mock` or by defining a list of service ids:
the "`happyr_service_mock`" service tag or by defining a list of service ids:

<details>
<summary>PHP config (Symfony 5.3)</summary>
<br>

```php
<?php
// config/packages/test/happyr_service_mocking.php

use Symfony\Config\HappyrServiceMockingConfig;

return static function (HappyrServiceMockingConfig $config) {
$config->services([
\App\AcmeApiClient::class,
\App\Some\OtherService::class,
]);
};

```

</details>
<details>
<summary>Yaml config</summary>
<br>

```yaml
# config/packages/test/happyr_service_mocking.yaml
@@ -47,10 +71,13 @@ happyr_service_mocking:
- 'App\Some\OtherService'
```
</details>
## Usage
```php
use App\AcmeApiClient;
use App\Some\OtherService;
use Happyr\ServiceMocking\ServiceMock;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

@@ -60,7 +87,7 @@ class MyTest extends WebTestCase
{
// ...

$apiClient = self::$container->get(AcmeApiClient::class);
$apiClient = self::getContainer()->get(AcmeApiClient::class);

// For all calls to $apiClient->show()
ServiceMock::all($apiClient, 'show', function ($id) {
@@ -70,7 +97,7 @@ class MyTest extends WebTestCase

// For only the next call to $apiClient->delete()
ServiceMock::next($apiClient, 'delete', function () {
return true
return true;
});

// This will queue a new callable for $apiClient->delete()
@@ -79,12 +106,15 @@ class MyTest extends WebTestCase
});

$mock = // create a PHPUnit mock or any other mock you want.
ServiceMock::swap(self::$container->get(MyService::class), $mock);
ServiceMock::swap(self::getContainer()->get(OtherService::class), $mock);

// ...
self::$client->request(...);
}

// To make sure we dont use affect other tests
protected function tearDown(): void
{
// To make sure we don't affect other tests
ServiceMock::resetAll();
// You can include the RestoreServiceContainer trait to automatically reset services
}
@@ -96,13 +126,35 @@ class MyTest extends WebTestCase
So how is this magic working?

When the container is built a new proxy class is generated from your service definition.
The proxy class acts and behaves just as the original. But on each method call if
The proxy class acts and behaves just as the original. But on each method call it
checks the `ProxyDefinition` if a custom behavior have been added.

With help from static properties, the `ProxyDefinition` will be remembered even if
the Kernel is rebooted.

## Limitations

This trick will not work if you have two different PHP processes, ie you are running
This trick will not work if you have two different PHP processes, i.e. you are running
your tests with Panther, Selenium etc.

We can also not create a proxy if your service is final.

We are only able to mock direct access to a service. Indirect method calls are not mocked.
Example:

```php
class MyService {
public function foo()
{
return $this->bar();
}

public function bar()
{
return 'original';
}
}
```

If we mock `MyService::bar()` to return `"mocked"`. You will still get `"orignal"`
when you call `MyService::foo()`. The workaround is to mock `MyService::foo()` too.
26 changes: 16 additions & 10 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
{
"name": "happyr/service-mocking",
"type": "symfony-bundle",
"description": "Make it easy to mock services in a built container",
"keywords": ["Symfony", "testing", "mock"],
"license": "MIT",
"type": "symfony-bundle",
"keywords": [
"Symfony",
"testing",
"mock"
],
"authors": [
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com"
}
],
"require": {
"php": ">=7.2",
"php": ">=8.1",
"friendsofphp/proxy-manager-lts": "^1.0",
"symfony/config": "^4.4 || ^5.1",
"symfony/dependency-injection": "^4.4 || ^5.1",
"symfony/http-kernel": "^4.4 || ^5.1"
"symfony/config": "^5.4 || ^6.0 || ^7.0",
"symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0",
"symfony/http-kernel": "^5.4 || ^6.0 || ^7.0"
},
"require-dev": {
"nyholm/symfony-bundle-test": "^2.0 || ^3.0",
"symfony/phpunit-bridge": "^6.4.11 || ^7.1.4"
},
"minimum-stability": "dev",
"prefer-stable": true,
"autoload": {
"psr-4": {
"Happyr\\ServiceMocking\\": "src/"
@@ -26,9 +36,5 @@
"psr-4": {
"Happyr\\ServiceMocking\\Tests\\": "tests/"
}
},
"require-dev": {
"nyholm/symfony-bundle-test": "^1.6",
"symfony/phpunit-bridge": "^4.4 || ^5.2"
}
}
12 changes: 11 additions & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
parameters:
ignoreErrors:
-
message: "#^Parameter \\#1 \\$autoload_function of function spl_autoload_register expects callable\\(string\\)\\: void, ProxyManager\\\\Autoloader\\\\AutoloaderInterface given\\.$#"
message: "#^Parameter \\#1 \\$callback of function spl_autoload_register expects \\(callable\\(string\\)\\: void\\)\\|null, ProxyManager\\\\Autoloader\\\\AutoloaderInterface given\\.$#"
count: 1
path: src/DependencyInjection/CompilerPass/ProxyServiceWithMockPass.php

@@ -15,3 +15,13 @@ parameters:
count: 1
path: src/Generator/LazyLoadingValueHolderGenerator.php

-
message: "#^Call to an undefined method ProxyManager\\\\Proxy\\\\LazyLoadingInterface\\:\\:getWrappedValueHolderValue\\(\\)\\.$#"
count: 1
path: src/ServiceMock.php

-
message: "#^Parameter \\#1 \\$initializer of method ProxyManager\\\\Proxy\\\\LazyLoadingInterface\\<object\\>\\:\\:setProxyInitializer\\(\\) expects \\(Closure\\(object\\|null\\=, ProxyManager\\\\Proxy\\\\LazyLoadingInterface\\<object\\>\\=, string\\=, array\\<string, mixed\\>\\=, Closure\\|null\\=, array\\<string, mixed\\>\\=\\)\\: bool\\)\\|null, Closure\\(mixed, ProxyManager\\\\Proxy\\\\LazyLoadingInterface, mixed, array, mixed\\)\\: true given\\.$#"
count: 1
path: src/ServiceMock.php

18 changes: 18 additions & 0 deletions psalm.baseline.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="5.26.1@d747f6500b38ac4f7dfc5edbcae6e4b637d7add0">
<file src="src/DependencyInjection/Configuration.php">
<UndefinedMethod>
<code><![CDATA[children]]></code>
</UndefinedMethod>
</file>
<file src="src/DependencyInjection/HappyrServiceMockingExtension.php">
<InternalClass>
<code><![CDATA[Extension]]></code>
</InternalClass>
</file>
<file src="src/ServiceMock.php">
<UndefinedInterfaceMethod>
<code><![CDATA[getWrappedValueHolderValue]]></code>
</UndefinedInterfaceMethod>
</file>
</files>
3 changes: 2 additions & 1 deletion psalm.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<?xml version="1.0"?>
<psalm
errorLevel="5"
errorLevel="4"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
errorBaseline="psalm.baseline.xml"
>
<projectFiles>
<directory name="src" />
20 changes: 15 additions & 5 deletions src/DependencyInjection/CompilerPass/ProxyServiceWithMockPass.php
Original file line number Diff line number Diff line change
@@ -11,8 +11,9 @@

class ProxyServiceWithMockPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
public function process(ContainerBuilder $container): void
{
/** @var array $serviceIds */
$serviceIds = $container->getParameter('happyr_service_mock.services');

foreach ($container->findTaggedServiceIds('happyr_service_mock') as $id => $tags) {
@@ -28,11 +29,11 @@ public function process(ContainerBuilder $container)
\spl_autoload_register($config->getProxyAutoloader());
$factory = new GeneratorFactory($config);

foreach ($serviceIds as $serviceId) {
foreach (array_unique($serviceIds) as $serviceId) {
if ($container->hasDefinition($serviceId)) {
$definition = $container->getDefinition($serviceId);
} elseif ($container->hasAlias($serviceId)) {
$definition = $container->getDefinition($container->getAlias($serviceId));
$definition = $container->getDefinition($container->getAlias($serviceId)->__toString());
} else {
throw new \LogicException(sprintf('[HappyrServiceMocking] Service or alias with id "%s" does not exist.', $serviceId));
}
@@ -41,9 +42,18 @@ public function process(ContainerBuilder $container)
return true;
};

$proxy = $factory->createProxy($definition->getClass(), $initializer);
$definition->setClass(get_class($proxy));
$proxy = $factory->createProxy($definition->getClass(), $initializer, ['skipDestructor' => true]);
$definition->setClass($proxyClass = get_class($proxy));
$definition->setPublic(true);
$definition->setLazy(false);

if (null !== $definition->getFactory()) {
$factoryMethod = $definition->getFactory();
$arguments = $definition->getArguments();
array_unshift($arguments, $factoryMethod);
$definition->setFactory([$proxyClass, '__construct_with_factory']);
$definition->setArguments($arguments);
}
}
}
}
5 changes: 1 addition & 4 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
@@ -7,10 +7,7 @@

class Configuration implements ConfigurationInterface
{
/**
* {@inheritdoc}
*/
public function getConfigTreeBuilder()
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('happyr_service_mocking');

5 changes: 1 addition & 4 deletions src/DependencyInjection/HappyrServiceMockingExtension.php
Original file line number Diff line number Diff line change
@@ -7,10 +7,7 @@

class HappyrServiceMockingExtension extends Extension
{
/**
* {@inheritdoc}
*/
public function load(array $configs, ContainerBuilder $container)
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
31 changes: 13 additions & 18 deletions src/Generator/Constructor.php
Original file line number Diff line number Diff line change
@@ -4,30 +4,25 @@

namespace Happyr\ServiceMocking\Generator;

use function array_filter;
use function array_map;
use function implode;
use Laminas\Code\Generator\Exception\InvalidArgumentException;
use Laminas\Code\Generator\PropertyGenerator;
use Laminas\Code\Reflection\MethodReflection;
use Laminas\Code\Reflection\ParameterReflection;
use ProxyManager\Generator\MethodGenerator;
use ProxyManager\ProxyGenerator\Util\Properties;
use ProxyManager\ProxyGenerator\Util\UnsetPropertiesGenerator;
use ReflectionClass;
use ReflectionMethod;
use function reset;
use function var_export;

/**
* The `__construct` implementation for lazy loading proxies.
*
* @interal
*/
class Constructor extends MethodGenerator
{
/**
* @throws InvalidArgumentException
*/
public static function generateMethod(ReflectionClass $originalClass, PropertyGenerator $valueHolder): self
public static function generateMethod(\ReflectionClass $originalClass, PropertyGenerator $valueHolder): self
{
$originalConstructor = self::getConstructor($originalClass);

@@ -39,7 +34,7 @@ public static function generateMethod(ReflectionClass $originalClass, PropertyGe
'static $reflection;'."\n\n"
.'if (! $this->'.$valueHolder->getName().') {'."\n"
.' $reflection = $reflection ?? new \ReflectionClass('
.var_export($originalClass->getName(), true)
.\var_export($originalClass->getName(), true)
.");\n"
.' $this->'.$valueHolder->getName().' = $reflection->newInstanceWithoutConstructor();'."\n"
.UnsetPropertiesGenerator::generateSnippet(Properties::fromReflectionClass($originalClass), 'this')
@@ -54,13 +49,13 @@ public static function generateMethod(ReflectionClass $originalClass, PropertyGe

private static function generateOriginalConstructorCall(
MethodReflection $originalConstructor,
PropertyGenerator $valueHolder
PropertyGenerator $valueHolder,
): string {
return "\n\n"
.'$this->'.$valueHolder->getName().'->'.$originalConstructor->getName().'('
.implode(
.\implode(
', ',
array_map(
\array_map(
static function (ParameterReflection $parameter): string {
return ($parameter->isVariadic() ? '...' : '').'$'.$parameter->getName();
},
@@ -70,23 +65,23 @@ static function (ParameterReflection $parameter): string {
.');';
}

private static function getConstructor(ReflectionClass $class): ?MethodReflection
private static function getConstructor(\ReflectionClass $class): ?MethodReflection
{
$constructors = array_map(
static function (ReflectionMethod $method): MethodReflection {
$constructors = \array_map(
static function (\ReflectionMethod $method): MethodReflection {
return new MethodReflection(
$method->getDeclaringClass()->getName(),
$method->getName()
);
},
array_filter(
\array_filter(
$class->getMethods(),
static function (ReflectionMethod $method): bool {
static function (\ReflectionMethod $method): bool {
return $method->isConstructor();
}
)
);

return reset($constructors) ?: null;
return \reset($constructors) ?: null;
}
}
2 changes: 2 additions & 0 deletions src/Generator/GeneratorFactory.php
Original file line number Diff line number Diff line change
@@ -10,6 +10,8 @@

/**
* Factory responsible of producing virtual proxy instances.
*
* @interal
*/
class GeneratorFactory extends LazyLoadingValueHolderFactory
{
36 changes: 14 additions & 22 deletions src/Generator/LazyLoadingValueHolderGenerator.php
Original file line number Diff line number Diff line change
@@ -4,11 +4,6 @@

namespace Happyr\ServiceMocking\Generator;

use function array_map;
use function array_merge;
use function func_get_arg;
use function func_num_args;
use InvalidArgumentException;
use Laminas\Code\Generator\ClassGenerator;
use Laminas\Code\Generator\MethodGenerator;
use Laminas\Code\Reflection\MethodReflection;
@@ -37,35 +32,31 @@
use ProxyManager\ProxyGenerator\Util\Properties;
use ProxyManager\ProxyGenerator\Util\ProxiedMethodsFilter;
use ProxyManager\ProxyGenerator\ValueHolder\MethodGenerator\GetWrappedValueHolderValue;
use ReflectionClass;
use ReflectionMethod;
use function str_replace;
use function substr;

/**
* Generator for proxies implementing {@see \ProxyManager\Proxy\VirtualProxyInterface}.
* Generator for proxies implementing {@see VirtualProxyInterface}.
*
* This is a 99% copy of ProxyManager\ProxyGenerator\LazyLoadingValueHolderGenerator.
* This class generates a different constructor.
*
* {@inheritDoc}
*
* @interal
*/
class LazyLoadingValueHolderGenerator implements ProxyGeneratorInterface
{
/**
* {@inheritDoc}
*
* @return void
*
* @throws InvalidProxiedClassException
* @throws InvalidArgumentException
* @throws \InvalidArgumentException
*
* @psalm-param array{skipDestructor?: bool, fluentSafe?: bool} $proxyOptions
*/
public function generate(ReflectionClass $originalClass, ClassGenerator $classGenerator/*, array $proxyOptions = []*/)
public function generate(\ReflectionClass $originalClass, ClassGenerator $classGenerator/* , array $proxyOptions = [] */)
{
/** @psalm-var array{skipDestructor?: bool, fluentSafe?: bool} $proxyOptions */
$proxyOptions = func_num_args() >= 3 ? func_get_arg(2) : [];
$proxyOptions = \func_num_args() >= 3 ? \func_get_arg(2) : [];

CanProxyAssertion::assertClassCanBeProxied($originalClass);

@@ -90,18 +81,19 @@ public function generate(ReflectionClass $originalClass, ClassGenerator $classGe
$excludedMethods[] = '__destruct';
}

array_map(
\array_map(
static function (MethodGenerator $generatedMethod) use ($originalClass, $classGenerator): void {
ClassGeneratorUtils::addMethodIfNotFinal($originalClass, $classGenerator, $generatedMethod);
},
array_merge(
array_map(
\array_merge(
\array_map(
$this->buildLazyLoadingMethodInterceptor($initializer, $valueHolder, $proxyOptions['fluentSafe'] ?? false),
ProxiedMethodsFilter::getProxiedMethods($originalClass, $excludedMethods)
),
[
new StaticProxyConstructor($initializer, Properties::fromReflectionClass($originalClass)),
Constructor::generateMethod($originalClass, $valueHolder), // Not a standard constructor
StaticConstructor::generateMethod($valueHolder), // Not a standard constructor
new MagicGet($originalClass, $initializer, $valueHolder, $publicProperties),
new MagicSet($originalClass, $initializer, $valueHolder, $publicProperties),
new MagicIsset($originalClass, $initializer, $valueHolder, $publicProperties),
@@ -123,9 +115,9 @@ static function (MethodGenerator $generatedMethod) use ($originalClass, $classGe
private function buildLazyLoadingMethodInterceptor(
InitializerProperty $initializer,
ValueHolderProperty $valueHolder,
bool $fluentSafe
bool $fluentSafe,
): callable {
return static function (ReflectionMethod $method) use ($initializer, $valueHolder, $fluentSafe): LazyLoadingMethodInterceptor {
return static function (\ReflectionMethod $method) use ($initializer, $valueHolder, $fluentSafe): LazyLoadingMethodInterceptor {
$byRef = $method->returnsReference() ? '& ' : '';
$method = LazyLoadingMethodInterceptor::generateMethod(
new MethodReflection($method->getDeclaringClass()->getName(), $method->getName()),
@@ -136,11 +128,11 @@ private function buildLazyLoadingMethodInterceptor(
if ($fluentSafe) {
$valueHolderName = '$this->'.$valueHolder->getName();
$body = $method->getBody();
$newBody = str_replace('return '.$valueHolderName, 'if ('.$valueHolderName.' === $returnValue = '.$byRef.$valueHolderName, $body);
$newBody = \str_replace('return '.$valueHolderName, 'if ('.$valueHolderName.' === $returnValue = '.$byRef.$valueHolderName, $body);

if ($newBody !== $body) {
$method->setBody(
substr($newBody, 0, -1).') {'."\n"
\substr($newBody, 0, -1).') {'."\n"
.' return $this;'."\n"
.'}'."\n\n"
.'return $returnValue;'
43 changes: 43 additions & 0 deletions src/Generator/StaticConstructor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace Happyr\ServiceMocking\Generator;

use Laminas\Code\Generator\Exception\InvalidArgumentException;
use Laminas\Code\Generator\ParameterGenerator;
use Laminas\Code\Generator\PropertyGenerator;
use ProxyManager\Generator\MethodGenerator;

/**
* The `__construct_with_factory` implementation for lazy loading proxies. This
* is used for services created with service factories.
*
* @interal
*/
class StaticConstructor extends MethodGenerator
{
/**
* @throws InvalidArgumentException
*/
public static function generateMethod(PropertyGenerator $valueHolder): self
{
$constructor = new self('__construct_with_factory');
$constructor->setStatic(true);
$constructor->setParameter(new ParameterGenerator('factory', 'callable'));
$parameter = new ParameterGenerator('arguments');
$parameter->setVariadic(true);
$constructor->setParameter($parameter);

$constructor->setBody(
'static $reflection;'."\n\n"
.'$reflection = $reflection ?? new \ReflectionClass(self::class);'."\n"
.'$model = $reflection->newInstanceWithoutConstructor();'."\n"
.'$model->'.$valueHolder->getName().' = \Closure::fromCallable($factory)->__invoke(...$arguments);'."\n"
.'\Happyr\ServiceMocking\ServiceMock::initializeProxy($model);'."\n\n"
.'return $model;'
);

return $constructor;
}
}
20 changes: 14 additions & 6 deletions src/Proxy/ProxyDefinition.php
Original file line number Diff line number Diff line change
@@ -21,19 +21,19 @@ public function __construct(object $originalObject)
$this->originalObject = $originalObject;
}

public function swap(object $replacement): void
{
$this->clear();
$this->replacement = $replacement;
}

public function clear(): void
{
$this->methods = [];
$this->methodsQueue = [];
$this->replacement = null;
}

public function swap(object $replacement): void
{
$this->clear();
$this->replacement = $replacement;
}

/**
* Get an object to execute a method on.
*/
@@ -47,6 +47,14 @@ public function getOriginalObject(): object
return $this->originalObject;
}

/**
* @internal
*/
public function setOriginalObject($originalObject): void
{
$this->originalObject = $originalObject;
}

public function getMethodCallable(string $method): ?callable
{
if (isset($this->methodsQueue[$method])) {
21 changes: 17 additions & 4 deletions src/ServiceMock.php
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@ public static function swap($proxy, object $replacement): void
$definition->swap($replacement);

// Initialize now so we can use it directly.
self::initializeProxy($proxy);
self::addInitializer($proxy);
}

/**
@@ -35,7 +35,7 @@ public static function next($proxy, string $methodName, callable ...$func): void
}

// Initialize now so we can use it directly.
self::initializeProxy($proxy);
self::addInitializer($proxy);
}

/**
@@ -47,15 +47,15 @@ public static function all($proxy, string $methodName, callable $func): void
$definition->addMethod($methodName, $func);

// Initialize now so we can use it directly.
self::initializeProxy($proxy);
self::addInitializer($proxy);
}

/**
* Reset all services.
*/
public static function resetAll(): void
{
foreach (static::$definitions as $definition) {
foreach (self::$definitions as $definition) {
$definition->clear();
}
}
@@ -79,7 +79,20 @@ public static function resetMethod($proxy, string $methodName): void
$definition->clearMethodsQueue($methodName);
}

/**
* This method is called in the proxy's constructor.
*
* @internal
*/
public static function initializeProxy(LazyLoadingInterface $proxy): void
{
$definition = self::getDefinition($proxy);
// Make sure the definition always have the latest original object.
$definition->setOriginalObject($proxy->getWrappedValueHolderValue());
self::addInitializer($proxy);
}

private static function addInitializer(LazyLoadingInterface $proxy): void
{
$initializer = function (&$wrappedObject, LazyLoadingInterface $proxy, $calledMethod, array $parameters, &$nextInitializer) {
$nextInitializer = null;
7 changes: 5 additions & 2 deletions src/Test/RestoreServiceContainer.php
Original file line number Diff line number Diff line change
@@ -2,8 +2,10 @@

namespace Happyr\ServiceMocking\Test;

use Happyr\ServiceMocking\ServiceMock;

/**
* After each tests, make sure we restore the default behavior of all
* After each test, make sure we restore the default behavior of all
* services.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
@@ -12,7 +14,8 @@ trait RestoreServiceContainer
{
/**
* @internal
* @before
*
* @after
*/
public static function _restoreContainer(): void
{
127 changes: 93 additions & 34 deletions tests/Functional/BundleInitializationTest.php
Original file line number Diff line number Diff line change
@@ -6,74 +6,133 @@

use Happyr\ServiceMocking\HappyrServiceMockingBundle;
use Happyr\ServiceMocking\ServiceMock;
use Nyholm\BundleTest\BaseBundleTestCase;
use Nyholm\BundleTest\CompilerPass\PublicServicePass;
use Happyr\ServiceMocking\Tests\Resource\ExampleService;
use Happyr\ServiceMocking\Tests\Resource\ServiceWithFactory;
use Happyr\ServiceMocking\Tests\Resource\StatefulService;
use Nyholm\BundleTest\TestKernel;
use ProxyManager\Proxy\VirtualProxyInterface;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\HttpKernel\KernelInterface;

class BundleInitializationTest extends BaseBundleTestCase
class BundleInitializationTest extends KernelTestCase
{
protected function setUp(): void
protected static function getKernelClass(): string
{
parent::setUp();
$this->addCompilerPass(new PublicServicePass('|router|'));
return TestKernel::class;
}

protected function getBundleClass()
protected static function createKernel(array $options = []): KernelInterface
{
return HappyrServiceMockingBundle::class;
if (null !== static::$kernel) {
return static::$kernel;
}
/**
* @var TestKernel $kernel
*/
$kernel = parent::createKernel($options);
$kernel->setClearCacheAfterShutdown(false);
$kernel->addTestBundle(HappyrServiceMockingBundle::class);
$configFile = $options['config_file'] ?? 'config.yml';
$kernel->addTestConfig(__DIR__.'/'.$configFile);
unset($options['config_file']);

$kernel->handleOptions($options);

return $kernel;
}

public function testInitBundle()
{
$kernel = $this->createKernel();
$kernel->addConfigFile(__DIR__.'/config.yml');
$kernel = self::bootKernel();
$container = $kernel->getContainer();

// Boot the kernel.
$this->bootKernel();

// Get the container
$container = $this->getContainer();

$this->assertTrue($container->has('router'));
$service = $container->get('router');
$this->assertTrue($container->has(ExampleService::class));
$service = $container->get(ExampleService::class);

$this->assertInstanceOf(Router::class, $service);
$this->assertInstanceOf(ExampleService::class, $service);
$this->assertInstanceOf(VirtualProxyInterface::class, $service);

$called = false;
ServiceMock::next($service, 'warmUp', function ($dir) use (&$called) {
ServiceMock::next($service, 'getNumber', function ($dir) use (&$called) {
$called = true;
$this->assertSame('foo', $dir);
$this->assertSame(11, $dir);

return 17;
});

$service->warmUp('foo');
$this->assertSame(17, $service->getNumber(11));
$this->assertTrue($called);

$mock = $this->getMockBuilder(\stdClass::class)
->disableOriginalConstructor()
->addMethods(['warmUp'])
->addMethods(['getNumber'])
->getMock();
$mock->expects($this->once())->method('warmUp')->willReturn(true);
$mock->expects($this->once())->method('getNumber')->willReturn(2);
ServiceMock::swap($service, $mock);

$this->assertTrue($service->warmUp('foo'));
$this->assertSame(2, $service->getNumber());

$serviceWithFactory = $container->get(ServiceWithFactory::class);
$this->assertSame(3, $serviceWithFactory->getSecretNumber());

$called = false;
ServiceMock::next($serviceWithFactory, 'getNumber', function ($dir) use (&$called) {
$called = true;
$this->assertSame(11, $dir);

return 17;
});

$this->assertSame(17, $serviceWithFactory->getNumber(11));
$this->assertTrue($called);
$this->assertSame(14, $serviceWithFactory->getNumber(11));
}

public function testRebootBundle()
{
$kernel = self::bootKernel();
$container = $kernel->getContainer();

$this->assertTrue($container->has(StatefulService::class));
$service = $container->get(StatefulService::class);
$service->setData('foobar');
$this->assertNotNull($service->getData());
ServiceMock::next($service, 'getData', function () {
return 'secret';
});

$this->bootKernel();

$container = $this->getContainer();
$service = $container->get(StatefulService::class);
$this->assertSame('secret', $service->getData());
$this->assertNull($service->getData());
}

public function testInitEmptyBundle()
public function testReloadRealObjectOnRebootBundle()
{
$kernel = $this->createKernel();
$kernel->addConfigFile(__DIR__.'/empty.yml');
$kernel = self::bootKernel();
$container = $kernel->getContainer();

// Boot the kernel.
$this->assertTrue($container->has(StatefulService::class));
$service = $container->get(StatefulService::class);
$service->setData('foobar');
$this->assertNotNull($service->getData());
$this->bootKernel();

// Get the container
$container = $this->getContainer();
$service = $container->get(StatefulService::class);
$this->assertNull($service->getData(), 'The real service object is not reloaded on kernel reboot.');
}

public function testInitEmptyBundle()
{
$kernel = self::bootKernel(['config_file' => 'empty.yml']);
$container = $kernel->getContainer();

$this->assertTrue($container->has('router'));
$service = $container->get('router');
$this->assertTrue($container->has(ExampleService::class));
$service = $container->get(ExampleService::class);

$this->assertInstanceOf(Router::class, $service);
$this->assertInstanceOf(ExampleService::class, $service);
}
}
20 changes: 19 additions & 1 deletion tests/Functional/config.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
happyr_service_mocking:
services:
- 'router'
- 'Happyr\ServiceMocking\Tests\Resource\ExampleService'
- 'Happyr\ServiceMocking\Tests\Resource\ServiceWithFactory'
- 'Happyr\ServiceMocking\Tests\Resource\ServiceWithDestructor'

services:

Happyr\ServiceMocking\Tests\Resource\ExampleService:
public: true

Happyr\ServiceMocking\Tests\Resource\ServiceWithDestructor:
public: true

Happyr\ServiceMocking\Tests\Resource\StatefulService:
tags:
- { name: happyr_service_mock }

Happyr\ServiceMocking\Tests\Resource\ServiceWithFactory:
factory: [Happyr\ServiceMocking\Tests\Resource\ServiceWithFactory, create]
arguments: [6,3]
4 changes: 4 additions & 0 deletions tests/Functional/empty.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
services:

Happyr\ServiceMocking\Tests\Resource\ExampleService:
public: true
13 changes: 13 additions & 0 deletions tests/Resource/ExampleService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Happyr\ServiceMocking\Tests\Resource;

class ExampleService
{
public function getNumber(int $input = 0): int
{
return 4711 + $input;
}
}
12 changes: 12 additions & 0 deletions tests/Resource/ServiceWithDestructor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Happyr\ServiceMocking\Tests\Resource;

class ServiceWithDestructor
{
public function __destruct()
{
}
}
34 changes: 34 additions & 0 deletions tests/Resource/ServiceWithFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Happyr\ServiceMocking\Tests\Resource;

class ServiceWithFactory
{
private $number;
private $secretNumber;

public function __construct(int $number)
{
$this->number = $number;
}

public static function create(int $number, int $secret)
{
$self = new self($number);
$self->secretNumber = $secret;

return $self;
}

public function getNumber(int $input = 0): int
{
return $this->number + $input - $this->secretNumber;
}

public function getSecretNumber(): int
{
return $this->secretNumber;
}
}
20 changes: 20 additions & 0 deletions tests/Resource/StatefulService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Happyr\ServiceMocking\Tests\Resource;

class StatefulService
{
private $data;

public function getData()
{
return $this->data;
}

public function setData($data): void
{
$this->data = $data;
}
}
Empty file removed tests/Unit/.gitignore
Empty file.
23 changes: 23 additions & 0 deletions tests/Unit/Proxy/ProxyDefinitionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Happyr\ServiceMocking\Tests\Unit\Proxy;

use Happyr\ServiceMocking\Proxy\ProxyDefinition;
use PHPUnit\Framework\TestCase;

class ProxyDefinitionTest extends TestCase
{
public function testSwap()
{
$a = new \stdClass();
$proxy = new ProxyDefinition($a);
$this->assertSame($a, $proxy->getObject());

$b = new \stdClass();
$proxy->swap($b);
$this->assertSame($b, $proxy->getObject());

$proxy->clear();
$this->assertSame($a, $proxy->getObject());
}
}