I've just started using PHPSpec and I'm really enjoying it over PHPUnit, especially the no-effort mocks and stubs. Anyway, the method I'm trying to test expects an array of Cell objects. How can I tell PHPSpec to give me an array of mocks?
Simplified version of my class
<?php
namespace Mything;
class Row
{
/** #var Cell[] */
protected $cells;
/**
* #param Cell[] $cells
*/
public function __construct(array $cells)
{
$this->setCells($cells);
}
/**
* #param Cell[] $cells
* #return Row
*/
public function setCells(array $cells)
{
// validate that $cells only contains instances of Cell
$this->cells = $cells;
return $this;
}
}
Simplified version of my test
<?php
namespace spec\MyThing\Row;
use MyThing\Cell;
use PhpSpec\ObjectBehavior;
class RowSpec extends ObjectBehavior
{
function let()
{
// need to get an array of Cell objects
$this->beConstructedWith($cells);
}
function it_is_initializable()
{
$this->shouldHaveType('MyThing\Row');
}
// ...
}
I had hoped I could do the following, but it then complains that it can't find Cell[]. Using the FQN it complains about not being able to find \MyThing\Cell[].
/**
* #param Cell[] $cells
*/
function let($cells)
{
// need to get an array of Cell objects
$this->beConstructedWith($cells);
}
The only options I can work out is to pass multiple type-hinted Cell arguments and manually combine them into an array. Am I missing something simple?
Edit: I'm using PHPSpec 2.5.3 as, unfortunately the server is currently stuck at PHP 5.3 :-(
Why don't you do something like
use Prophecy\Prophet;
use Cell; // adapt it with PSR-4 and make it use correct class
class RowSpec extends ObjectBehavior
{
private $prophet;
private $cells = [];
function let()
{
$this->prophet = new Prophet();
for ($i = 0; $i < 10; $i++) {
$this->cells[] = $this->prophet->prophesize(Cell::class);
}
$this->beConstructedWith($cells);
}
// ....
function letGo()
{
$this->prophet->checkPredictions();
}
public function it_is_a_dummy_spec_method()
{
// use here your cells mocks with $this->cells
// and make predictions on them
}
}
Explanation
In let function you instantiate a Prophet object that is, basically a mocking library/framework that is used in tandem with PHPSpec (that itself use Prophecy).
I suggest to keep the instance ($this->prophet) as will be useful for next steps.
Now, you have to create your mocks, and you can do with prophet and prophesize.
Even for the mocks, I suggest to keep them into a private variable that you probably use for predictions in your methods.
letGo function is here to check explicitly the expectations you have made on cells: without, cells are only stubs or dummies.
Of course it's handy to pass through method signature a mock and to skip checkPredictions explicitly but, as soon as you need an array of mocks, I suppose that this is the only way to reach your goal.
Related
Consider having a Class whose method is being called in iteration n times, and the method in itself has a statement that pulls data from a cache system - Redis.
Now if this method is called n times the cache is also hit n times, leading to a continuous fetch and unserializing of the same data n times, which in the case of an interpreter like PHP consumes a good amount of time & CPU.
Passing the cached data to this method could also be a no, as we might instantiate n number of instances of this class.
Hence is there a way where we can avoid hitting the cache multiple times in the context of a Class and/or Object?
Maybe we can somehow use static properties of the Object to hold the value?
First, write a service class that:
Provides getter, which:
Loads required value from cache (based on unique-key),
But also backups the loaded-value,
Finally, returns backupped-value instead of re-loading from cache.
Provides setter which updates both backup and cache.
Then simply use Laravel's feature to inject and/or get instance of said service-class.
Example
<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
class MyCacheService
{
protected $backupMap = [];
/**
* Requests an instance of this-class from Laravel.
*
* #return MyCacheService|null
*/
public static function instance()
{
return \Illuminate\Support\Facades\App::make(MyCacheService::class);
}
public function get($key)
{
$backup = & $this->backupMap[$key];
if ( ! isset($backup)) {
$backup = $this->rawGet($key);
}
return $buckup;
}
public function set($key, $value)
{
$this->rawSet($key, $value);
$this->backupMap[$key] = $value;
}
/** Loads from cache */
private function rawGet($key)
{
// ... your normal loading from cache logic goes here,
// but as sub-example:
return Cache::get($key);
}
/** Saves into cache */
private function rawSet($key, $value)
{
// ... your normal saving into cache logic goes here,
// but as sub-example:
Cache::set($key, $value);
}
}
Usage:
use App\Services\MyCacheService;
use Illuminate\Support\Facades\App;
// ...
// Get instance once, like:
$cache = MyCacheService::instance();
// Or like:
/** #var MyCacheService|null $cache */
$cache = App::make(MyCacheService::class);
// Finally, reuse as many times as required, like:
$myValue = $cache->get('my-unique-key');
$myOtherValue = $cache->get('my-other-key');
Note that Laravel stores the class's instance for us, else one could use private static property named $instance, and return that from instance() method.
WARNING: as you may already know, maybe replace rawGet and rawSet's logic.
Also, place MyCacheService.php file (which's source is above) in app/Services folder.
If you are in the opinion that you should not use a service-class (which the other answer explains),
then consider using PHP's $GLOBALS variable to backup the loaded-Cache (instead of adding yet another custom static variable).
Examples
Maybe backup once for all:
In Laravel's AppServiceProvider.php file, do something like:
<?php
namespace App\Providers;
// ...
use Illuminate\Support\Facades\Cache;
class AppServiceProvider extends ServiceProvider
{
// ...
public function boot()
{
$keyList = [
'my-unique-key',
'my-other-key',
];
foreach($keyList as $key) {
$GLOBALS['cache-' . $key] = Cache::get($key);
}
}
}
Finally, use $GLOBALS['cache-my-unique-key'] anywhere required.
Or, backup per class:
<?php
use Illuminate\Support\Facades\Cache;
class YourClass {
private $myData;
public function __construct()
{
$key = 'my-unique-key';
$value = & $GLOBALS['cache-' . $key];
if ( ! isset($value)) {
$value = Cache::get($key);
}
$this->myData = $value;
}
// Finally, use `$this->myData` anywhere in this same class.
}
Note that as you may already know, in both cases we use 'cache-' prefix to not overwrite anything by mistake.
You can use static properties to store the cached data and use access it same.
class CacheClass
{
private static $cachedData;
public function retrieveData()
{
if (!isset(self::$cachedData)) {
// re-assign the value
self::$cachedData = fetchDataFromCache();
}
return self::$cachedData;
}
}
I'm trying to implement a Results class that processes queries. So, simply put, you would have functions like this:
function all();
function first();
function paginate(int $perPage, int $pageNo = 1);
This works pretty well, the problem being that the IDE has no possible way of knowing the return type when this same results class is being used in multiple different query classes. Example:
UserQuery->results()->all() will return an array of User entities.
UserQuery->results()->first() will return a single User entity.
In some languages, you have generics, which means I could just use Results<User> in the UserQuery class and then my Results class could return T[] and T respectively.
One idea I had was to pass an empty entity as the constructor to the Results class and then try to use that property as the return type, but I couldn't figure this out. Is there any workaround to this? The main problem I'm trying to solve is IDE autocompletion and analysis, so a pure phpDoc solution is perfectly fine for my use case.
The only other workaround I can come up with is having to write a separate Results class for each entity type, which would prove to be exhausting.
I don't think there's a way to do exactly what you described, but in similar cases I would suggest using a proxy class for each Results type and document proper return types using phpDocumentor's #method. This solution has added value of having a great place for any type specific Results modifications and expansions. Here's an example:
abstract class Results
{
function all(): array
{
}
function first()
{
}
function paginate(int $perPage, int $pageNo = 1): array
{
}
}
class User { }
/**
* #method User[] all()
* #method User first()
* #method User[] paginate(int $perPage, int $pageNo = 1)
*/
class UserResults extends Results { }
class UserQuery
{
/**
* #var UserResults
*/
private $results;
public function __construct()
{
$this->results = new UserResults();
}
public function results(): UserResults
{
return $this->results;
}
}
$userQuery = new UserQuery();
$test = $userQuery->results()->all();
I am finding a lot of the time I pass an array of custom objects to a function for processing. In my function I check to make sure it is the right class before accessing internal properties/methods.
Because I am taking an array and as far as I am aware PHP doesn't have any typed generic list I cannot use type hinting so intellisense in my IDE doesn't work and code inspection throws warnings.
I came across an old post that gave the idea of throwing in this line of code in order to get intellisense working while developing:
if (false) $myObj = new MyObject();
So I end up with a function that looks like:
function ProcessObjectArray(array $arrayOfObject)
{
foreach ($arrayOfObject as $key => $myObj) {
if (get_class($myObj) == 'MyNamespace\MyObject') {
if (false) $myObj = new MyObject();
// I can now access intellisense for MyObject in here
} else {
trigger_error('is not right object');
}
}
}
It seems like a bit of a weird hack so I am wondering if this is a sign that I am not handling arrays of objects in the best way or if there is a better way to get my intellisense working. I had a look at the arrayobject interface to see if I could create a class implementing arrayobject that would hold a typed list. While it makes it nice to put all validation inside its constructor or append like functions I couldn't get it working with intellisense as the internal container is still just a standard array. Also using get_class doesn't seem good as if a class name or namespace is renamed then the IDE refactoring features do not pick this reference up (well, PHPStorm doesn't at least and it is one I am using).
I of course don't need intellisense but the weirdness of this hack made me wonder if I have the right approach to OOP in PHP and thought I might be missing something. Is this the right way of doing it or have I missed something?
with phpstorm when you add your annotations it will correctly do the code completion for you.
/**
* #param MyNamespace\MyObject[] $arrayOfObject
*/
function ProcessObjectArray(array $arrayOfObject)
{
foreach ($arrayOfObject as $key => $myObj) {
if (get_class($myObj) == 'MyNamespace\MyObject') {
if (false) $myObj = new MyObject();
// I can now access intellisense for MyObject in here
} else {
trigger_error('is not right object');
}
}
}
However intellisense won't stop you from passing in incorrect objects, it will only help you while coding to prevent passing in the incorrect objects.
Perhaps you may wish to consider using collections instead of using generic arrays.
There are lots of examples out there, however here is a simple one which you can expand upon
class Collection
{
/** #var array */
private $items = [];
/**
* #param MyNamespace\MyObject $obj
* #return $this
*/
public function addItem(MyNamespace\MyObject $obj) {
$this->items[] = $obj;
return $this;
}
/**
* #param $index
* #return $this
*/
public function deleteItem($index) {
unset($this->items[$index]);
return $this;
}
/**
* #param $index
* #return MyNamespace\MyObject|null
*/
public function getItem($index) {
return array_key_exists($index, $this->items) ? $this->items[$index] : null;
}
/**
* #return MyNamespace\MyObject[]
*/
public function getItems() {
return $this->items;
}
/**
* #return int
*/
public function count() {
return sizeOf($this->items);
}
}
here is an example how it all works:
$collection = new \Collection;
$obj = new MyNamespace\MyObject;
$collection->addItem($obj);
foreach($collection->getItems() as $item) {
// this will already know that $item is MyNamespace\MyObject because of the annotation on the getItems() method
$item->doSomething();
}
class MyCollection implements \Iterator
{
/**
* #var CollectionElement[]
*/
private $elements = [];
public function addElement(CollectionElement $element)
{
$this->elements[] = $element;
}
/**
* #return CollectionElement
*/
public function current()
{
return current($this->elements);
}
//... Rest of the Iterator implementation
}
This way you cannot add to the collection anything other than CollectionElement. And after:
foreach($collection as $element){
/** #var CollectionElement $element */
$element->//here you will have autocompletion
}
You don't need to check anything since your collection will never contain anything you are not expecting.
If you are going this route, I would avoid using the are native arrays. I would make classes that contain arrays of a certain type. That way, you can validate the array contents in the constructor of the class, and then you can use actual type hinting for the objects, which PHP allows.
I'm curious about the best way of speccing classes that handle file operations.
Assuming I have a fictional class with a method duplicate whose job is to duplicate the contents of a file.
<?php
class FileOperator
{
public function duplicate($filename)
{
$content = file_get_contents($filename);
file_put_contents($filename, $content.$content);
}
}
I know that I can use something like vfsStream to assert the change without touching the actual filesystem (at least with assertions in PHPUnit).
How could I assert that in a spec? Or would it be approached differently?
Also, I get that I might want to extract that functionality into another class and use a Spy to assert that the FileOperator calls its dependency correctly, but then I'd still have to spec that adapter class, and my question remains.
Thanks.
This is more likely a functional test rather than an unit test, so it's hard to use phpspec in this case.
If you insist, I see two options.
If you happen to need a method to fetch the file contents too, you could write your spec this way:
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamDirectory;
use PhpSpec\ObjectBehavior;
class FileOperatorSpec extends ObjectBehavior
{
/**
* #var vfsStreamDirectory
*/
private $workDir;
function let()
{
$this->workDir = vfsStream::setup('workDir');
}
function it_duplicates_a_content_in_a_file()
{
$this->createFile('foo', 'bar');
$this->duplicate('vfs://workDir/foo');
$this->read('vfs://workDir/foo')->shouldReturn('barbar');
}
private function createFile($path, $content)
{
$file = vfsStream::newFile($path);
$file->setContent($content);
$this->workDir->addChild($file);
}
}
Alternatively, you could use the expect helper:
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamDirectory;
use PhpSpec\ObjectBehavior;
class FileOperatorSpec extends ObjectBehavior
{
/**
* #var vfsStreamDirectory
*/
private $workDir;
function let()
{
$this->workDir = vfsStream::setup('workDir');
}
function it_duplicates_a_content_in_a_file()
{
$this->createFile('foo', 'bar');
$this->duplicate('vfs://workDir/foo');
expect(file_get_contents('vfs://workDir/foo'))->toBe('barbar');
}
private function createFile($path, $content)
{
$file = vfsStream::newFile($path);
$file->setContent($content);
$this->workDir->addChild($file);
}
}
I don't know how to phrase this one exactly but what I need is a way to load the type-hinting of a classes methods. Basically I have a base class that has a get function that looks like this:
class Base {
/**
*
* #param type $i
* #return \i
*/
public static function get($i) {
// make sure it exists before creating
$classes = get_declared_classes();
if (!in_array($i, $classes)) {
if (class_exists($i)) {
return new $i();
}
}
return $i;
}
}
Now for an example, say I had a class called test:
class test {
function derp() {
echo 'derp';
}
}
I'd instantiate the test object by something like this:
$test = base::get('test');
Now what I'd like to be able to do is as I type like this:
$test->
The methods (Currently only derp()) should be suggested, I've seen documents all around SO but they don't work :(
What's weird is that if I change the #return comment to the test class name then the suggestions work.
BUT it the classes are all not set, there could be different classes instantiated, hence why I tried #returns \i (suggested by netbeans). Is there any way to achieve this?
EDIT
The reason I need the type hinting is to allow for calling methods like the following:
base::get('test')->derp();
What always works is this:
/** #var ADDTYPEHERE $test */
$test = base::get('test');
What also works is this:
if ($test instanceof CLASS_IT_IS) {
// completion works in here
}
The solution you want can never work, since your IDE (netbeans) cannot know what class you instantiated, without any hint like one of the above.