nyholm/nsa
Testing helper to access and manipulate private/protected properties and methods in PHP. Set/get instance or static properties and invoke hidden methods to simplify tests and improve DX. Install via Composer: nyholm/nsa.
Installation:
composer require --dev nyholm/nsa
Ensure it’s added to require-dev in composer.json to avoid production inclusion.
First Use Case: Test a private method in a Laravel Eloquent model:
use Nyholm\NSA\NSA;
$user = new User();
NSA::setProperty($user, 'email_verified_at', now());
$result = NSA::invokeMethod($user, 'validateVerification', ['email']);
$this->assertTrue($result);
Where to Look First:
NSA::getProperty(), NSA::setProperty(), NSA::invokeMethod(), and NSA::getConstant().boot() logic, accessors/mutators).PrivateService::processOrder()).Model::incrementing).Testing Private Model Methods:
// Test a private model method (e.g., password hashing)
$user = new User(['password' => 'plaintext']);
NSA::invokeMethod($user, 'hashPassword');
$this->assertTrue(Hash::check('plaintext', $user->password));
Setting Private Properties in Fixtures:
// Bypass protected property for test data
$user = User::factory()->create();
NSA::setProperty($user, 'api_token', 'test-token-123');
Invoking Static Methods:
// Test static logic in a base class
$config = NSA::invokeMethod('\App\Services\ConfigService', 'getDefault');
Reading Constants:
// Access private constants (e.g., in a trait)
$maxRetries = NSA::getConstant('\App\Traits\Retryable', 'MAX_RETRIES');
Bulk Property Access:
// Debug all private properties (e.g., during test development)
$properties = NSA::getProperties($object);
dd($properties);
Test-Driven Development (TDD):
// test/Unit/UserTest.php
public function test_private_verification_logic()
{
$user = new User();
NSA::setProperty($user, 'verification_token', 'abc123');
$result = NSA::invokeMethod($user, 'verifyToken', ['abc123']);
$this->assertTrue($result);
}
Debugging Complex Objects:
NSA::getProperties() to inspect the state of a model or service during test development.$order = Order::find(1);
dd(NSA::getProperties($order)); // Inspect private properties
Integration with Laravel Factories:
User::factory()->state([
'name' => 'Test User',
])->create()->afterCreating(function ($user) {
NSA::setProperty($user, 'is_admin', true);
});
Testing Abstract Classes:
Illuminate\Database\Eloquent\Model):
$incrementing = NSA::getProperty('\App\Models\User', 'incrementing');
$this->assertFalse($incrementing);
Avoid in Production:
autoload:
# .github/workflows/ci.yml
- name: Block NSA in production
run: |
if grep -r "Nyholm\\\\NSA" src/; then
echo "NSA found in src/ directory!"
exit 1
fi
Cache Closures for Performance:
$closure = NSA::getClosure($object, 'privateMethod');
$result1 = $closure($arg1);
$result2 = $closure($arg2);
Document NSA Usage:
// NSA: Testing private method due to circular dependency with ServiceA
NSA::invokeMethod($service, 'internalProcess', [$data]);
Pair with Refactoring:
// Step 1: Test private method with NSA
NSA::invokeMethod($validator, 'validateInternal', [$data]);
// Step 2: Refactor to public API
$validator->validateBehavior($data);
Laravel-Specific:
$user = new User();
NSA::invokeMethod($user, 'fireModelEvent', ['creating']);
Reflection Limitations:
ReflectionClass directly or refactor to instance properties.
// ❌ Won't work
NSA::setProperty('\App\Models\User', 'staticProperty', 'value');
// ✅ Workaround
$reflection = new ReflectionClass('\App\Models\User');
$property = $reflection->getProperty('staticProperty');
$property->setAccessible(true);
$property->setValue(null, 'value');
PHP 8+ Behavior Changes:
Performance Overhead:
$closure = NSA::getClosure($object, 'expensiveMethod');
Brittle Tests:
Laravel-Specific Risks:
Illuminate\Events\Dispatcher) may break across versions.app()->make() first:
$service = app()->make('\App\Services\PrivateService');
NSA::invokeMethod($service, 'internalMethod');
Thread Safety:
Verify Property/Method Exists:
ReflectionClass to debug:
$reflection = new ReflectionClass($object);
if (!$reflection->hasMethod('privateMethod')) {
$this->fail('Method does not exist');
}
Handle Exceptions:
try {
NSA::invokeMethod($object, 'nonexistentMethod');
$this->fail('Expected exception');
} catch (Exception $e) {
$this->assertStringContainsString('Method not found', $e->getMessage());
}
Inspect Object State:
NSA::getProperties() to debug:
$properties = NSA::getProperties($object);
$this->assertArrayHasKey('privateField', $properties);
Static Property Access:
if (!class_exists('\App\Models\User')) {
$this->markTestSkipped('Class not loaded');
}
$value = NSA::getProperty('\App\Models\User', 'staticProperty');
Custom NSA Instance:
class CustomNSA extends NSA
{
public static function getProtectedProperty($object, string $property)
{
return parent::getProperty($object, $property);
}
}
Integration with Pest:
// tests/TestHelper.php
use Nyholm\NSA\NSA;
function nsa($object)
How can I help you explore Laravel packages today?