understanding pimple php source code - php

Pimple is a simple dependency injection container in php used in silex framework. I was going through the source code here. In the documentation the function offsetGet returns the same instance of the class that is attached to the dependency container. the relevant code for offsetGet is :
public function offsetGet($id)
{
if (!isset($this->keys[$id])) {
throw new InvalidArgumentException(sprintf('Identifier "%s" is not defined.', $id));
}
if (
isset($this->raw[$id])
|| !is_object($this->values[$id])
|| isset($this->protected[$this->values[$id]])
|| !method_exists($this->values[$id], '__invoke')
) {
return $this->values[$id];
}
if (isset($this->factories[$this->values[$id]])) {
return $this->values[$id]($this);
}
$this->frozen[$id] = true;
$this->raw[$id] = $this->values[$id];
return $this->values[$id] = $this->values[$id]($this);
}
Here, if the object is in the factories Object Store(SplObjectStorage type), it returns a new instance of the class with id $id. then in the last return again $this->values[$id] is set to a new instance of the object and that new instance is returned.
return $this->values[$id] = $this->values[$id]($this).
This is the line I fail to understand. How is this line supposed to return the same instance for different calls of offsetGet for the same $id. Won't it return a new instance every time?
Please help me. I tried a lot but I don't get it.

I looked at the source code of pimple and found out that once the object is instantiated and kept in $this->values[$id], the next call of offsetGet will return from the second if condition.
i.e this if condition:
if (
isset($this->raw[$id])
|| !is_object($this->values[$id])
|| isset($this->protected[$this->values[$id]])
|| !method_exists($this->values[$id], '__invoke')
) {
return $this->values[$id];
}
Looking at the unit tests, i found out that the objects without the magic method __invoke can be shared . If the object has a magic method __invoke(i.e the object can be treated as a function) , a new instance is returned everytime.
So, you can see that the first, second and third condition on the above if statement return false . but the fourth condition returns true and hence the $this->values[$id] returns the same instance every time.

Related

Calling methods on object gets 'call to member function on array' error

I inherited this project from my predecessor, and he was way overqualified. A lot of stuff he wrote goes over my head. But as far as vanilla php goes, I'm pretty confident, and can't for the life of me figure out why the application thinks the object I created is an array. Maybe I don't actually know anything. You tell me.
use via\zoom\Bulletin;
use via\zoom\DatabaseConnection;
require_once('includes/config.php');
require_once(CORE .'sql.php');
require_once(CORE . 'model.php');
require_once(CORE . 'bulletin.php');
// If we've passed the validation step we can guarantee we have a valid $active_user
validate();
//run if a page deletion has been requested
if (isset($_GET['delpage'])) {
$del = $_GET['delpage'];
$bulletin = new Bulletin;
$bulletin = Bulletin::get($del);
if(!empty($bulletin))
{
$bulletin->delete();
/*
So.
For some reason, the above object is cast as an array.
If you try to cast it as an object, it defaults to stdClass.
On the left we have a method complaining that it can't work outside of its class. Hard stop, array to method exception.
On the right we have an object with all the right data, but set to the wrong class, so it can't find the delete method at all. Hard stop, undefined method exception.
*/
//this is the workaround, pulled the script straight from the delete method in the model class
/*$dbh = DatabaseConnection::get();
$query_string = "DELETE FROM brochure_generator_bulletin WHERE id = $del";
try {
$dbh->query($query_string);
//return true;
} catch (\Exception $e) {
//return false;
}*/
}
header('Location: bulletins');
exit();
}
Here's the get method from the Bulletin class, extends Model--
public static function get( ...$ids )
{
$matches = parent::get( ...$ids );
foreach( $matches as &$match )
{
$match->content = json_decode( $match->content );
}
return $matches;
}
And here's the delete method from the Model Class:
public function delete()
{
if (isset($this->id)) {
$dbh = DatabaseConnection::get();
$query_string = "DELETE FROM {$this->table_name} WHERE id = \"{$this->id}\"";
try {
$dbh->query($query_string);
return true;
} catch (\Exception $e) {
return false;
}
}
return false;
}
What am I missing? Is he using a framework I'm not familiar with? I'm utterly grasping at straws here, and at this point my options are grab all the method scripts and stick them where they need to be inline, or just starting over from the ground up.
You don't need to create a new Bulletin object before using the static get() method, so you can remove this:
$bulletin = new Bulletin;
That $bulletin variable is immediately overwritten by the next line anyway.
$bulletin = Bulletin::get($del);
get() takes one or more ids and returns an array of one or more corresponding objects. You're giving it one id and expecting one object back, but it's still going to return that object inside an array. You just need to get the object out of the array so you can call its delete method.
if(!empty($bulletin))
{
$bulletin = reset($bulletin); // get the first item in the array
$bulletin->delete();
You could also review the model and see if it has a different method that returns a single object rather than an array of objects.

Encoding clone $this in JsonSerializable

This simplified case is resulting in a PHP segfault (exit 127):
class Datum implements \JsonSerializable{
public function jsonSerialize(){
return clone $this;
}
}
echo json_encode(new Datum);
The last line of code results in exit(127). I'm unable to retrieve any stack in my current environment.
Meanwhile, removing the clone token works.
Is there any possible explanation why this is happening?
This code results in an infinite recursion.
It appears that the PHP JSON module supports JsonSerializable in this manner (pseudocode):
function json_encode($data){
if($data instanceof JsonSerializable) return json_encode($data->jsonSerialize());
else real_json_encode($data); // handling primitive data or arrays or pure data objects
}
If you return yet another instance of JsonSerializable, json_encode is going to try to serialize it again, resulting in an infinite recursion.
This is working for return $this;, however, probably due to intentional workaround from json_encode's implementation where it goes straight to real json_encode when the returned object is identical, i.e. when $this is returned. However this is not happening for cloned objects since $a !== clone $a.
References
This answer can be supported by reference from the php-src.
// in php_json_encode_zval
if (instanceof_function(Z_OBJCE_P(val), php_json_serializable_ce)) {
return php_json_encode_serializable_object(buf, val, options, encoder);
}
// in php_json_encode_serializable_object
if ((Z_TYPE(retval) == IS_OBJECT) &&
(Z_OBJ(retval) == Z_OBJ_P(val))) {
/* Handle the case where jsonSerialize does: return $this; by going straight to encode array */
PHP_JSON_HASH_APPLY_PROTECTION_DEC(myht);
return_code = php_json_encode_array(buf, &retval, options, encoder);
} else {
/* All other types, encode as normal */
return_code = php_json_encode_zval(buf, &retval, options, encoder);
PHP_JSON_HASH_APPLY_PROTECTION_DEC(myht);
}
These snippets prove that PHP would encode return $this; as an array (or as a non-serializable object), while returning anything else makes Z_OBJ(retval) == Z_OBJ_P(val) false, going to the else block which recursively calls php_json_encode_zval again.
TL;DR, Simple solution: return (array) $this; instead of clone $this;.

getting started with mocking in PHP

How do I get started with mocking a web service in PHP? I'm currently directly querying the web API's in my unit testing class but it takes too long. Someone told me that you should just mock the service. But how do I go about that? I'm currently using PHPUnit.
What I have in mind is to simply save a static result (json or xml file) somewhere in the file system and write a class which reads from that file. Is that how mocking works? Can you point me out to resources which could help me with this. Is PHPUnit enough or do I need other tools? If PHPUnit is enough what part of PHPUnit do I need to check out? Thanks in advance!
You would mock the web service and then test what is returned. The hard coded data you are expecting back is correct, you set the Mock to return it, so then additional methods of your class may continue to work with the results. You may need Dependency Injection as well to help with the testing.
class WebService {
private $svc;
// Constructor Injection, pass the WebService object here
public function __construct($Service = NULL)
{
if(! is_null($Service) )
{
if($Service instanceof WebService)
{
$this->SetIWebService($Service);
}
}
}
function SetWebService(WebService $Service)
{
$this->svc = $Service
}
function DoWeb($Request)
{
$svc = $this->svc;
$Result = $svc->getResult($Request);
if ($Result->success == false)
$Result->Error = $this->GetErrorCode($Result->errorCode);
}
function GetErrorCode($errorCode) {
// do stuff
}
}
Test:
class WebServiceTest extends PHPUnit_Framework_TestCase
{
// Simple test for GetErrorCode to work Properly
public function testGetErrorCode()
{
$TestClass = new WebService();
$this->assertEquals('One', $TestClass->GetErrorCode(1));
$this->assertEquals('Two', $TestClass->GetErrorCode(2));
}
// Could also use dataProvider to send different returnValues, and then check with Asserts.
public function testDoWebSericeCall()
{
// Create a mock for the WebService class,
// only mock the getResult() method.
$MockService = $this->getMock('WebService', array('getResult'));
// Set up the expectation for the getResult() method
$MockService->expects($this->any())
->method('getResult')
->will($this->returnValue(1)); // Change returnValue to your hard coded results
// Create Test Object - Pass our Mock as the service
$TestClass = new WebService($MockService);
// Or
// $TestClass = new WebService();
// $TestClass->SetWebServices($MockService);
// Test DoWeb
$WebString = 'Some String since we did not specify it to the Mock'; // Could be checked with the Mock functions
$this->assertEquals('One', $TestClass->DoWeb($WebString));
}
}
This mock may then be used in the other functions since the return is hard coded, your normal code would process the results and perform what work the code should (Format for display, etc...). This could also then have tests written for it.

Fast check if an object will be successfully instantiated in PHP?

How can I check if an object will be successfully instantiated with the given argument, without actually creating the instance?
Actually I'm only checking (didn't tested this code, but should work fine...) the number of required parameters, ignoring types:
// Filter definition and arguments as per configuration
$filter = $container->getDefinition($serviceId);
$args = $activeFilters[$filterName];
// Check number of required arguments vs arguments in config
$constructor = $reflector->getConstructor();
$numRequired = $constructor->getNumberOfRequiredParameters();
$numSpecified = is_array($args) ? count($args) : 1;
if($numRequired < $numSpecified) {
throw new InvalidFilterDefinitionException(
$serviceId,
$numRequired,
$numSpecified
);
}
EDIT: $constructor can be null...
The short answer is that you simply cannot determine if a set of arguments will allow error-free instantiation of a constructor. As commenters have mentioned above, there's no way to know for sure if a class can be instantiated with a given argument list because there are runtime considerations that cannot be known without actually attempting
instantiation.
However, there is value in trying to instantiate a class from a list of constructor arguments. The most obvious use-case for this sort of operation is a configurable Dependency Injection Container (DIC). Unfortunately, this is a much more complicated operation than the OP suggests.
We need to determine for each argument in a supplied definition array whether or not it matches specified type-hints from the constructor method signature (if the method signature actually has type-hints). Also, we need to resolve how to treat default argument values. Additionally, for our code to be of any real use we need to allow the specification of "definitions" ahead of time for instantiating a class. A sophisticated treatment of the problem will also involve a pool of reflection objects (caching) to minimize the performance impact of repeatedly reflecting things.
Another hurdle is the fact that there's no way to access the type-hint of a reflected method parameter without calling its ReflectionParameter::getClass method and subsequently instantiating a reflection class from the returned class name (if null is returned the param has no type-hint). This is where caching generated reflections becomes particularly important for any real-world use-case.
The code below is a severely stripped-down version of my own string-based recursive dependency injection container. It's a mixture of pseudo-code and real-code (if you were hoping for free code to copy/paste you're out of luck). You'll see that the code below matches the associative array keys of "definition" arrays to the parameter names in the constructor signature.
The real code can be found over at the relevant github project page.
class Provider {
private $definitions;
public function define($class, array $definition) {
$class = strtolower($class);
$this->definitions[$class] = $definition;
}
public function make($class, array $definition = null) {
$class = strtolower($class);
if (is_null($definition) && isset($this->definitions[$class])) {
$definition = $this->definitions[$class];
}
$reflClass = new ReflectionClass($class);
$instanceArgs = $this->buildNewInstanceArgs($reflClass);
return $reflClass->newInstanceArgs($instanceArgs);
}
private function buildNewInstanceArgs(
ReflectionClass $reflClass,
array $definition
) {
$instanceArgs = array();
$reflCtor = $reflClass->getConstructor();
// IF no constructor exists we're done and should just
// return a new instance of $class:
// return $this->make($reflClass->name);
// otherwise ...
$reflCtorParams = $reflCtor->getParameters();
foreach ($reflCtorParams as $ctorParam) {
if (isset($definition[$ctorParam->name])) {
$instanceArgs[] = $this->make($definition[$ctorParam->name]);
continue;
}
$typeHint = $this->getParameterTypeHint($ctorParam);
if ($typeHint && $this->isInstantiable($typeHint)) {
// The typehint is instantiable, go ahead and make a new
// instance of it
$instanceArgs[] = $this->make($typeHint);
} elseif ($typeHint) {
// The typehint is abstract or an interface. We can't
// proceed because we already know we don't have a
// definition telling us which class to instantiate
throw Exception;
} elseif ($ctorParam->isDefaultValueAvailable()) {
// No typehint, try to use the default parameter value
$instanceArgs[] = $ctorParam->getDefaultValue();
} else {
// If all else fails, try passing in a NULL or something
$instanceArgs[] = NULL;
}
}
return $instanceArgs;
}
private function getParameterTypeHint(ReflectionParameter $param) {
// ... see the note about retrieving parameter typehints
// in the exposition ...
}
private function isInstantiable($class) {
// determine if the class typehint is abstract/interface
// RTM on reflection for how to do this
}
}

Mock in PHPUnit - multiple configuration of the same method with different arguments

Is it possible to configure PHPUnit mock in this way?
$context = $this->getMockBuilder('Context')
->getMock();
$context->expects($this->any())
->method('offsetGet')
->with('Matcher')
->will($this->returnValue(new Matcher()));
$context->expects($this->any())
->method('offsetGet')
->with('Logger')
->will($this->returnValue(new Logger()));
I use PHPUnit 3.5.10 and it fails when I ask for Matcher because it expects "Logger" argument.
It is like the second expectation is rewriting the first one, but when I dump the mock, everything looks ok.
Sadly this is not possible with the default PHPUnit Mock API.
I can see two options that can get you close to something like this:
Using ->at($x)
$context = $this->getMockBuilder('Context')
->getMock();
$context->expects($this->at(0))
->method('offsetGet')
->with('Matcher')
->will($this->returnValue(new Matcher()));
$context->expects($this->at(1))
->method('offsetGet')
->with('Logger')
->will($this->returnValue(new Logger()));
This will work fine but you are testing more than you should (mainly that it gets called with matcher first, and that is an implementation detail).
Also this will fail if you have more than one call to each of of the functions!
Accepting both parameters and using returnCallBack
This is more work but works nicer since you don't depend on the order of the calls:
Working example:
<?php
class FooTest extends PHPUnit_Framework_TestCase {
public function testX() {
$context = $this->getMockBuilder('Context')
->getMock();
$context->expects($this->exactly(2))
->method('offsetGet')
->with($this->logicalOr(
$this->equalTo('Matcher'),
$this->equalTo('Logger')
))
->will($this->returnCallback(
function($param) {
var_dump(func_get_args());
// The first arg will be Matcher or Logger
// so something like "return new $param" should work here
}
));
$context->offsetGet("Matcher");
$context->offsetGet("Logger");
}
}
class Context {
public function offsetGet() { echo "org"; }
}
This will output:
/*
$ phpunit footest.php
PHPUnit 3.5.11 by Sebastian Bergmann.
array(1) {
[0]=>
string(7) "Matcher"
}
array(1) {
[0]=>
string(6) "Logger"
}
.
Time: 0 seconds, Memory: 3.00Mb
OK (1 test, 1 assertion)
I've used $this->exactly(2) in the matcher to show that this does also work with counting the invocations. If you don't need that swapping it out for $this->any() will, of course, work.
As of PHPUnit 3.6, there is $this->returnValueMap() which may be used to return different values depending on the given parameters to the method stub.
You can achieve this with a callback:
class MockTest extends PHPUnit_Framework_TestCase
{
/**
* #dataProvider provideExpectedInstance
*/
public function testMockReturnsInstance($expectedInstance)
{
$context = $this->getMock('Context');
$context->expects($this->any())
->method('offsetGet')
// Accept any of "Matcher" or "Logger" for first argument
->with($this->logicalOr(
$this->equalTo('Matcher'),
$this->equalTo('Logger')
))
// Return what was passed to offsetGet as a new instance
->will($this->returnCallback(
function($arg1) {
return new $arg1;
}
));
$this->assertInstanceOf(
$expectedInstance,
$context->offsetGet($expectedInstance)
);
}
public function provideExpectedInstance()
{
return array_chunk(array('Matcher', 'Logger'), 1);
}
}
Should pass for any "Logger" or "Matcher" arguments passed to the Context Mock's offsetGet method:
F:\Work\code\gordon\sandbox>phpunit NewFileTest.php
PHPUnit 3.5.13 by Sebastian Bergmann.
..
Time: 0 seconds, Memory: 3.25Mb
OK (2 tests, 4 assertions)
As you can see, PHPUnit ran two tests. One for each dataProvider value. And in each of those tests it made the assertion for with() and the one for instanceOf, hence four assertions.
Following on from the answer of #edorian and the comments (#MarijnHuizendveld) regarding ensuring that the method is called with both Matcher and Logger, and not simply twice with either Matcher or Logger, here is an example.
$expectedArguments = array('Matcher', 'Logger');
$context->expects($this->exactly(2))
->method('offsetGet')
->with($this->logicalOr(
$this->equalTo('Matcher'),
$this->equalTo('Logger')
))
->will($this->returnCallback(
function($param) use (&$expectedArguments){
if(($key = array_search($param, $expectedArguments)) !== false) {
// remove called argument from list
unset($expectedArguments[$key]);
}
// The first arg will be Matcher or Logger
// so something like "return new $param" should work here
}
));
// perform actions...
// check all arguments removed
$this->assertEquals(array(), $expectedArguments, 'Method offsetGet not called with all required arguments');
This is with PHPUnit 3.7.
If the method you are testing doesn't actually return anything, and you simply need to test that it is called with the correct arguments, the same approach applies. For this scenario, I also attempted doing this using a callback function for $this->callback as the argument to the with, rather than returnCallback in the will. This fails, as internally phpunit calls the callback twice in the process of verifying the argument matcher callback. This means that the approach fails as on the second call that argument has already been removed from the expected arguments array. I don't know why phpunit calls it twice (seems an unnecessary waste), and I guess you could work around that by only removing it on the second call, but I wasn't confident enough that this is intended and consistent phpunit behaviour to rely on that occurring.
My 2 cents to the topic: pay attention when using at($x): it means that expected method call will be the ($x+1)th method call on the mock object; it doesn't mean that will be the ($x+1)th call of the expected method. This made me waste some time so I hope it won't with you. Kind regards to everyone.
I just stumbled on this PHP extension to mock objects: https://github.com/etsy/phpunit-extensions/wiki/Mock-Object
Here are also some solutions with the doublit library :
Solution 1 : using Stubs::returnValueMap
/* Get a dummy double instance */
$double = Doublit::dummy_instance(Context::class);
/* Test the "offsetGet" method */
$double::_method('offsetGet')
// Test that the first argument is equal to "Matcher" or "Logger"
->args([Constraints::logicalOr('Matcher', 'Logger')])
// Return "new Matcher()" when first argument is "Matcher"
// Return "new Logger()" when first argument is "Logger"
->stub(Stubs::returnValueMap([['Matcher'], ['Logger']], [new Matcher(), new Logger()]));
Solution 2 : using a callback
/* Get a dummy double instance */
$double = Doublit::dummy_instance(Context::class);
/* Test the "offsetGet" method */
$double::_method('offsetGet')
// Test that the first argument is equal to "Matcher" or "Logger"
->args([Constraints::logicalOr('Matcher', 'Logger')])
// Return "new Matcher()" when first argument $arg is "Matcher"
// Return "new Logger()" when first argument $arg is "Logger"
->stub(function($arg){
if($arg == 'Matcher'){
return new Matcher();
} else if($arg == 'Logger'){
return new Logger();
}
});

Categories