PHPackages                             robvanaarle/php-object-seam - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Testing &amp; Quality](/categories/testing)
4. /
5. robvanaarle/php-object-seam

ActiveLibrary[Testing &amp; Quality](/categories/testing)

robvanaarle/php-object-seam
===========================

An easy way to create object seams to break dependencies with minimal code changes in order to test legacy PHP code

v1.3.0(4mo ago)24.8k↓100%1BSD-3-ClausePHPPHP &gt;=7.0CI passing

Since Jun 6Pushed 4mo ago2 watchersCompare

[ Source](https://github.com/robvanaarle/php-object-seam)[ Packagist](https://packagist.org/packages/robvanaarle/php-object-seam)[ RSS](/packages/robvanaarle-php-object-seam/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (5)Dependencies (3)Versions (7)Used By (0)

PHP Object Seam
===============

[](#php-object-seam)

*A lightweight toolkit for introducing object seams into legacy PHP code to make it testable - with minimal or no changes to the Class Under Test.*

Legacy PHP code can be difficult to extend and test because dependencies are tightly coupled and often hidden behind private methods, static helpers, or heavy constructors. Although refactoring is ideal, complexity and time constraints often prevent it. To safely add new features or fix bugs, automated tests must come first - but those same dependencies make adding tests hard.

In *Working Effectively with Legacy Code*, Michael Feathers defines a **seam** as *“a place to alter program behavior, without changing the code.”*
This library provides an implementation of **object seams**, enabling you to:

- Invoke private/protected behavior
- Override methods, static methods and hooks at runtime
- Capture calls for assertions
- Instantiate objects without running their original constructors

All **without or minimal modifications to the original class**.

Installation
------------

[](#installation)

`composer require --dev robvanaarle/php-object-seam:^1`

Requirements
------------

[](#requirements)

PHP &gt;= 7.0. This package supports a wide range of PHP versions to accommodate legacy codebases.

Example
-------

[](#example)

```
class TemperatureApi
{
    public function getCurrentTemperature(string $location): float
    {
        $weatherData = $this->getWeatherData($location);

        if ('unknown_location' === $weatherData['error']) {
            throw new \InvalidArgumentException("Unknown location: {$location}");
        }
        if (null !== $weatherData['error']) {
            throw new \RuntimeException("Weather API error: {$weatherData['error']}");
        }

        return $this->fahrenheitToCelsius($weatherData['current']['temp_f']);
    }

    private function fahrenheitToCelsius(float $fahrenheit): float
    {
        return ($fahrenheit - 32) * 5 / 9;
    }

    private function getWeatherData(string $location): array
    {
        $weatherData = json_decode(file_get_contents("http://api.weatherapi.com/v1/{$location}/current"), true);
        return $weatherData;
    }
}
```

Testing `TemperatureApi` is difficult because the only public method makes an actual HTTP request, which is problematic because it is slow, unreliable, and may incur costs. It should be refactored if possible, but when that is not an option, we can use object seams to test it.

Without modifying the class, we can use an object seam to call the private method `fahrenheitToCelsius()` directly to test it:

```
public function testFahrenheitToCelsiusAtFreezingPoint(): void
{
    $api = $this->createObjectSeam(TemperatureApi::class);

    // Call the protected method via the seam.
    static::assertEquals(0.0, $api->seam()->call('fahrenheitToCelsius', 32.0));
}
```

The error handling of `getCurrentTemperature()` can be tested by first making a small change ('incision') to the `TemperatureApi` class: make `getWeatherData()` protected. This allows for overriding the method to return controlled data:

```
public function testUnknownLocationThrowsException(): void
{
    $api = $this->createObjectSeam(TemperatureApi::class);
    // $api behaves exactly like TemperatureApi, but we can override behavior.

    // Override the getWeatherData method to simulate an unknown location.
    // Method must be protected or public to be overridden.
    $api->seam()->override('getWeatherData', function (string $location) {
        return ['error' => 'unknown_location'];
    });

    $this->expectException(\InvalidArgumentException::class);
    $this->expectExceptionMessage('Unknown location: Atlantis');

    $api->getCurrentTemperature('Atlantis');
}
```

Features
--------

[](#features)

These capabilities map directly to Feathers’ dependency-breaking techniques: 'Subclass and Make Public', 'Subclass and Override' and 'Expose Static Method'.

### Introspection &amp; Invocation

[](#introspection--invocation)

- Call **protected/private methods**
- Call **protected static methods**
- Call **protected/private property** get/set hooks

### Behavior Overrides

[](#behavior-overrides)

- Override **public/protected methods**
- Override **public/protected static methods**
- Override **public/protected property hooks**

### Call Capturing

[](#call-capturing)

- Capture calls to public/protected methods
- Capture calls to public/protected static methods
- Capture calls to public/protected property hooks

### Object Construction

[](#object-construction)

- Instantiate objects **without executing** the original constructor
- Provide a **custom constructor**
- Defer construction and call it later

### Developer Experience

[](#developer-experience)

- Autocomplete support in PhpStorm using `CreatesObjectSeams`
- Testing-framework agnostic

Why Not Create Seams Manually?
------------------------------

[](#why-not-create-seams-manually)

Manually creating seams usually involves writing boilerplate subclasses or duplicated logic.
PHP Object Seam provides:

- **Less code** - most seam logic is generated for you
- **Clearer test intent** - overrides are explicit
- **Faster test authoring**
- **Reusable, configurable seam instances** that can be adapted per test case

Basic Usage in tests
--------------------

[](#basic-usage-in-tests)

```
class CurrencyApiTest
{
    use PHPObjectSeam\CreatesObjectSeams;

    public function testExample(): void
    {
        // Creates an CurrencyApi&ObjectSeam object - the constructor is not executed
        $api = $this->createObjectSeam(CurrencyApi::class);
        // $api behaves exactly like CurrencyApi, but we can override behavior.

        $api->seam()
          ->override('connect', fn ($username, $password) => 'dummy_token')
          ->customConstruct(function($arg1) {
              $this->url = 'http://www.dummy.url/' . $arg1;
          }, 'api/v1/');

        // do something with $api
        static::assertEquals('dummy_token', $api->getToken());
    }
}
```

Usage Guide
-----------

[](#usage-guide)

### Call non-public method

[](#call-non-public-method)

```
$foo = $this->createObjectSeam(Foo::class);
$result = $foo->seam()->call('nonPublicMethod', $arg1, $arg2);
```

### Call protected static method

[](#call-protected-static-method)

```
$foo = $this->createObjectSeam(Foo::class);
$result = $foo->seam()->callStatic('protectedStaticMethod', $arg1, $arg2);
```

### Call non-public property hook

[](#call-non-public-property-hook)

```
$foo = $this->createObjectSeam(Foo::class);
$foo->seam()->call('$nonPublicProperty::set', $value);
$value = $foo->seam()->call('$nonPublicProperty::get');
```

### Override public or protected method

[](#override-public-or-protected-method)

Overridden methods are executed in the scope of the object seam class.

Override with a Closure:

```
$foo = $this->createObjectSeam(Foo::class);
$foo->seam()->override('publicOrProtectedMethod', function(int $arg1) {
    return $this->otherMethod($arg1) * 5;
});
```

Override with a result value:

```
$foo = $this->createObjectSeam(Foo::class);
$foo->seam()->override('publicOrProtectedMethod', 42);
```

### Override public or protected static method

[](#override-public-or-protected-static-method)

Overridden static methods are executed in the scope of the object seam class.

Override with a Closure:

```
$foo = $this->createObjectSeam(Foo::class);
$foo->seam()->overrideStatic('publicOrProtectedStaticMethod', function(int $arg1) {
    return parent::protectedMethod($arg1) * 3;
});
```

Override with a result value:

```
$foo = $this->createObjectSeam(Foo::class);
$foo->seam()->overrideStatic('publicOrProtectedStaticMethod', 9);
```

### Override public or protected property hook

[](#override-public-or-protected-property-hook)

Overridden property hooks are executed in the scope of the object seam class.

Override with a Closure:

```
$foo = $this->createObjectSeam(Foo::class);
$foo->seam()->override('$publicOrProtectedProperty::set', function(int $value) {
    $this->value = $value * 2;
})->override('$publicOrProtectedProperty::get', function() {
    return $this->value + 10;
});
```

Override with a result value:

```
$foo = $this->createObjectSeam(Foo::class);
$foo->seam()->override('$publicOrProtectedProperty::get', 25);
```

### Instantiate an object with a custom constructor

[](#instantiate-an-object-with-a-custom-constructor)

```
$foo = $this->createObjectSeam(Foo::class);
$foo->seam()->customConstruct(function($arg1) {
    $this->url = 'http://www.dummy.url/' . $arg1;
}, 'api/v1/');
```

or set a custom constructor and call it later:

```
// i.e. in the setup of your test
$this->foo = $this->createObjectSeam(Foo::class);
$this->foo->seam()->setCustomConstructor(function($arg1) {
    $this->url = 'http://www.dummy.url/' . $arg1;
});

// in a specific test case
$this->foo->callCustomConstructor('api/v1/');
```

### Call original constructor

[](#call-original-constructor)

Often there is no need for a custom constructor; in that case, the original constructor can be called.

```
$foo = $this->createObjectSeam(Foo::class);
$foo->__construct('bar');

// or via the seam interface
$foo->seam()->call('__construct', 'bar');
$foo->seam()->callConstruct('bar');
```

### Capture and retrieve public and protected method calls

[](#capture-and-retrieve-public-and-protected-method-calls)

Capturing and retrieving calls allows for asserting that a method has been called and with which arguments.

```
$foo = $this->createObjectSeam(Foo::class);
$foo->seam()->captureCalls('publicOrProtectedMethod');

// do something with $foo
$foo->methodThatUsesTheCapturingMethods();

$calls = $foo->seam()->getCapturedCalls('publicOrProtectedMethod');
// assert that $calls contains a certain combination of arguments.
```

### Capture and retrieve public and protected static method calls

[](#capture-and-retrieve-public-and-protected-static-method-calls)

Capturing and retrieving static calls allows for asserting that a method has been called and with which arguments.

```
$foo = $this->createObjectSeam(Foo::class);
$foo->seam()->captureStaticCalls('publicOrProtectedStaticMethod');

// do something with $foo
$foo::methodThatUsesTheCapturingMethods();

$calls = $foo->seam()->getCapturedStaticCalls('publicOrProtectedStaticMethod');
// assert that $calls contains a certain combination of arguments.
```

### Capture and retrieve public and protected property hook calls

[](#capture-and-retrieve-public-and-protected-property-hook-calls)

Capturing and retrieving calls allows for asserting that a method has been called and with which arguments.

```
$foo = $this->createObjectSeam(Foo::class);
$foo->seam()->captureCalls('$publicOrProtectedProperty::get')
    ->captureCalls('$publicOrProtectedProperty::set');

// do something with $foo
$foo->methodThatUsesTheCapturingProperties();

$getCalls = $foo->seam()->getCapturedCalls('$publicOrProtectedProperty::get');
$setCalls = $foo->seam()->getCapturedCalls('$publicOrProtectedProperty::set');
// assert that $calls contains a certain combination of arguments.
```

###  Health Score

42

—

FairBetter than 90% of packages

Maintenance74

Regular maintenance activity

Popularity26

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~322 days

Total

5

Last Release

145d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/9f71b13994265a4abfa3686ccf9474cbe3779db5104168b21171ab3a49132cb4?d=identicon)[robvanaarle](/maintainers/robvanaarle)

---

Top Contributors

[![robvanaarle](https://avatars.githubusercontent.com/u/2284220?v=4)](https://github.com/robvanaarle "robvanaarle (19 commits)")

---

Tags

unittestlegacyseam

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP\_CodeSniffer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/robvanaarle-php-object-seam/health.svg)

```
[![Health](https://phpackages.com/badges/robvanaarle-php-object-seam/health.svg)](https://phpackages.com/packages/robvanaarle-php-object-seam)
```

###  Alternatives

[phpspec/prophecy

Highly opinionated mocking framework for PHP 5.3+

8.5k551.7M678](/packages/phpspec-prophecy)[rregeer/phpunit-coverage-check

Check the code coverage using the clover report of phpunit

606.1M179](/packages/rregeer-phpunit-coverage-check)[codeception/module-asserts

Codeception module containing various assertions

8550.6M1.2k](/packages/codeception-module-asserts)[friendsofcake/fixturize

CakePHP Fixture classes to help increase productivity or performance

24504.7k1](/packages/friendsofcake-fixturize)[sofa/eloquent-testsuite

Helpers for fast and reliable UNIT tests for your Eloquent Models with PHPUnit

10104.7k](/packages/sofa-eloquent-testsuite)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
