I have a class which acts like a storage (add/get item). I try to bind it as a singleton in one service provider, and resolve it in another's boot method.
The code is changed for simplicity.
app/Providers/BindingProvider.php
<?php namespace App\Providers;
use Illuminate\Support\Facades\Facade;
use Illuminate\Support\ServiceProvider as ServiceProvider;
class MyBindingFacade extends Facade {
public static function getFacadeAccessor() {
return 'my.binding';
}
}
class MyBinding {
protected $items = [];
public function add($name, $item) {
$this->items[$name] = $item;
}
public function get($name) {
return $this->items[$name];
}
public function getAll() {
return $this->items;
}
}
class BindingProvider extends ServiceProvider {
public function register() {
$this->app->singleton('my.binding', function($app) {
return $app->make('App\Providers\MyBinding');
});
}
public function provides() {
return [
'my.binding',
];
}
}
app/Providers/ResolvingProvider.php
<?php namespace App\Providers;
use Illuminate\Support\ServiceProvider as ServiceProvider;
use App\Providers\MyBinding;
class ResolvingProvider extends ServiceProvider {
public function boot(MyBinding $binding) {
$binding->add('foo', 'bar');
// $manual = $this->app->make('my.binding');
// $manual->add('foo', 'bar');
}
public function register() {}
}
app/Http/Controllers/WelcomeController.php
<?php
namespace App\Http\Controllers;
use App\Providers\MyBindingFacade;
class WelcomeController extends Controller {
public function index()
{
dd(MyBindingFacade::getAll()); // debug items
}
}
When I try to debug MyBinding state in my WelcomeController I'm getting empty item array. However, if I uncomment $manual part from my ResolvingProvider it returns an array containing 'foo' => 'bar'. Does it mean IoC resolution is broken in ServiceProvider::boot() method or am I misusing Laravel functionality?
Laravel version: 5.0.28
UPDATE: Added code sample from WelcomeController.
With this:
$this->app->singleton('my.binding', function($app) {
return $app->make('App\Providers\MyBinding');
});
You're saying: my.binding is a singleton and resolves to an instance of App\Providers\MyBinding.
That doesn't mean that App\Providers\MyBinding is registered as singleton too. What you should do instead is this:
$this->app->singleton('App\Providers\MyBinding');
$this->app->bind('my.binding', function($app) {
return $app->make('App\Providers\MyBinding');
});
Because the Facade binding uses $app->make() you should get the same instance you registered with $this->app->singleton() right above.
In the first example you are not using the Facade, you should be using:
use App\Providers\MyBindingFacade as MyBinding;
Which will in fact call make it using 'my.binding'.
Related
I have made a simple class named MyService
namespace App\Services;
class MyService
{
private $anotherService;
public function setService(AnotherService $anotherService)
{
$this->anotherService = $anotherService;
}
public function getService()
{
if(empty($this->anotherService)){
$this->setService(new AnotherService());
}
return $this->anotherService;
}
public function call()
{
$anotherService = $this->getService();
$anotherService->SetXY(5,10);
}
}
As you can se via a setter I set as Depedency the AnotherService:
namespace App\Services;
class AnotherService
{
public function SetXY($x,$y)
{
}
}
In order to test whether the MyService runs as expected I made the following test:
namespace Tests\Services;
namespace Tests\Services;
use App\Services\MyService;
use App\Services\AnotherService;
use PHPUnit\Framework\TestCase;
use Mockery;
class MyServiceTest extends TestCase
{
public function testService()
{
$mockedAnotherService = Mockery::spy(AnotherService::class);
$mockedAnotherService->shouldReceive('SetXY');
$service = new MyService();
$service->setService($mockedAnotherService);
$service->call();
$mockedAnotherService->shouldHaveReceived()->setXY(5,10);
}
}
But for some reason seems that I am unable to assert that setXY is called despite the opposite. The error is:
1) Tests\Services\MyServiceTest::testService
Mockery\Exception\InvalidCountException: Method setXY(<Any Arguments>) from Mockery_0_App_Services_AnotherService should be called
at least 1 times but called 0 times.
/var/www/html/api/vendor/mockery/mockery/library/Mockery/CountValidator/AtLeast.php:47
/var/www/html/api/vendor/mockery/mockery/library/Mockery/Expectation.php:310
/var/www/html/api/vendor/mockery/mockery/library/Mockery/ReceivedMethodCalls.php:46
/var/www/html/api/vendor/mockery/mockery/library/Mockery/VerificationDirector.php:36
/var/www/html/api/vendor/mockery/mockery/library/Mockery/HigherOrderMessage.php:46
/var/www/html/api/tests/Services/MyServiceTest.php:23
phpvfscomposer:///var/www/html/api/vendor/phpunit/phpunit/phpunit:60
Do you know why that does happen?
There is a typo in:
$mockedAnotherService->shouldHaveReceived()->setXY(5,10);
Should be:
$mockedAnotherService->shouldHaveReceived()->SetXY(5,10);
I'm trying to make use of a base interface for all my other interfaces as follows:
Base interface
<?php
namespace App\Repositories\Data;
interface IDataRepository
{
public function getAll();
public function getById($id);
public function create($model);
public function update($model);
public function delete($id);
}
Implemented base interface
<?php namespace App\Repositories\Data;
use Illuminate\Database\Eloquent\Model;
class DataRepository implements IDataRepository
{
// model property on class instances
protected $model;
// Constructor to bind model to repo
public function __construct(Model $model)
{
$this->model = $model;
}
// Get all instances of model
public function getAll()
{
return $this->model->all();
}
// create a new record in the database
public function create($model)
{
return $this->model->create($model);
}
// update record in the database
public function update($model)
{
$record = $this->find($model.id);
return $record->update($model);
}
// remove record from the database
public function delete($id)
{
return $this->model->destroy($id);
}
// show the record with the given id
public function getById($id)
{
return $this->model-findOrFail($id);
}
}
The interface where i'm trying to make use of the base interface
<?php
namespace App\Repositories;
use App\Repositories\Data\IDataRepository;
interface ITestRepository extends IDataRepository
{
}
implementation
<?php namespace App\Repositories;
use App\Library\Classes\Test;
use Illuminate\Database\Eloquent\Model;
class TestRepository implements ITestRepository
{
}
In my controller i'm trying to just call test repository so i can use all the base repository functions:
class TestController extends Controller
{
protected $testRepository;
public function __construct(Test $test)
{
$this->testRepository = new TestRepository($test);
}
public function index()
{
$data['testData'] = $this->testRepository->getAll();
return view('test', $data);
}
}
But i get the following error:
Class App\Repositories\TestRepository contains 5 abstract methods and
must therefore be declared abstract or implement the remaining methods
My application works fine if i only make use of my base interface and pass through a model. What would be the correct way to share functions from my base interface across all my other interfaces, so as to prevent code duplication? I appreciate any help.
I think that a Trait which will contains all methods of your interface declaration is the best choice. Something like (not sure about logic):
namespace App\Repositories;
trait TDataRepository
{
// model property on class instances
protected $model;
// Constructor to bind model to repo
public function __construct(Model $model)
{
$this->model = $model;
}
// Get all instances of model
public function getAll()
{
return $this->model->all();
}
// create a new record in the database
public function create($model)
{
return $this->model->create($model);
}
// update record in the database
public function update($model)
{
$record = $this->find($model.id);
return $record->update($model);
}
// remove record from the database
public function delete($id)
{
return $this->model->destroy($id);
}
// show the record with the given id
public function getById($id)
{
return $this->model-findOrFail($id);
}
}
And then just use it for classes with base interface:
namespace App\Repositories;
use App\Library\Classes\Test;
use Illuminate\Database\Eloquent\Model;
class TestRepository implements ITestRepository
{
use TDataRepository;
}
Also there are some other options:
abstract class with methods for base interface but it not so flexible like trait,
composition but you should change base idea and create a new entity for composition.
<?php
namespace App\Repositories;
use App\Interfaces\ITestRepository;
class TestRepository implements ITestRepository
{
public function getAll()
{
// TODO: Implement getAll() method.
}
public function getById($id)
{
// TODO: Implement getById() method.
}
public function create($model)
{
// TODO: Implement create() method.
}
public function update($model)
{
// TODO: Implement update() method.
}
public function delete($id)
{
// TODO: Implement delete() method.
}
}
Class must be declared abstract or implement methods 'getAll', 'getById', 'update', 'create', 'delete'
So All the method is by default abstract method in interface and you have to define all method in this class.
The class TestRepository should not implement any interface, but extend DataRepository:
<?php namespace App\Repositories;
use App\Repositories\Data\DataRepository;
class TestRepository extends DataRepository
{
}
DataRepository contains already the implementation of the interface IDataRepository. When you create a class implementing ITestRepository you will have to define the implementation of all the methods in the interface (which are the same as the base interface, in your case).
I'm building a Lumen app. I'm trying to use interface for my repositories. All my logic is wrapped in a composer package.
Here's my vendor/package/src/app/Providers/PackageServiceProvider.php:
<?php
namespace Vendor\Package\App\Providers;
use Illuminate\Support\ServiceProvider;
class SmsBackendCoreServiceProvider extends ServiceProvider
{
protected $defer = false;
public function register()
{
$this->app->bind(
'Vendor\Package\App\Repositories\Contracts\SmsService',
'Vendor\Package\App\Repositories\Services\SmsJson'
);
}
public function boot()
{
$this->app->group(
['namespace' => 'Vendor\Package\App\Http\Controllers'],
function ($app) {
require __DIR__.'/../../routes/web.php';
}
);
}
}
Here is my vendor/package/src/routes/web.php:
<?php
$app->get('/sms/send.json', 'JsonController#send');
Here is my vendor/package/src/app/Http/Controllers/JsonController.php:
<?php
namespace Vendor\Package\App\Http\Controllers;
use Vendor\Package\App\Http\Controllers\BaseController;
use Vendor\Package\App\Repositories\Contracts\SmsService;
class JsonController extends BaseController
{
public $service;
public function __construct(SmsService $service)
{
$this->service = $service;
}
public function send()
{
$response = $this->service->sendSms(1, 2, 3);
return $response;
}
}
Here is my vendor/package/src/app/Repositories/Contracts/SmsService.php:
<?php
namespace Vendor\Package\App\Repositories\Contracts;
class SmsService
{
public function sendSMS($from, $to, $text);
}
Finally, here is my vendor/package/src/app/Repositories/Services/SmsJson.php:
<?php
namespace Vendor\Package\App\Repositories\Services;
use Vendor\Package\App\Repositories\Contracts\SmsService;
class SmsJson implements SmsService
{
public function sendSMS($from, $to, $text)
{
echo 'success';
}
}
When I try to access http://mydomain.dev/sms/send.json, I get this error:
FatalErrorException in SmsService.php line 7: Non-abstract method
Mitto\SmsBackendCore\App\Repositories\Contracts\SmsService::sendSMS()
must contain body
Where did I go wrong?
Your contract is declared as a class, it must be an interface.
I am about $this->close() to giving up on Mockery in my unit tests. Here's what's going on, I am working with Laravel 5.1 and I'm trying to test my repository pattern abstraction using Mockery in PHPUnit. I've followed the tutorials, poured over the StackOverflow questions so it's not a duplicate. When you see anything about modules here, it's PingPong Sky Modules package.
Basically, when I try to mock the repository interface and set shouldReceive('create')->with([])->once() , Mockery throws:
Mockery\Exception\InvalidCountException: Method create(array()) from Mockery_0_Modules_Documents_Repositories_DocumentRepositoryInterface should be called exactly 1 times but called 0 times.
DocumentsTest.php
<?php
use Illuminate\Foundation\Testing\WithoutMiddleware;
class FileUploadTest extends TestCase {
use WithoutMiddleware;
public function mock($class){
$mock = Mockery::mock($class);
$this->app->instance($class, $mock);
return $mock;
}
public function testFileUpload(){
// Mock the Repository
$mock = $this->mock('\Modules\Documents\Repositories\DocumentRepositoryInterface');
$mock->shouldReceive('create')->with([])->once();
$this->call('POST', '/documents', [], [], []);
$this->assertResponseStatus(201);
}
}
DocumentRepositoryInterface.php
<?php namespace Modules\Documents\Repositories;
interface DocumentRepositoryInterface {
public function create(array $data);
}
DatabaseDocumentRepository.php
<?php namespace Modules\Documents\Repositories;
use Modules\Documents\Repositories\DocumentRepositoryInterface;
use \Illuminate\Database\Eloquent\Model;
class DatabaseDocumentRepository implements DocumentRepositoryInterface {
protected $documents;
public function __construct(Model $documents) {
$this->documents = $documents;
}
public function create(array $data) {
// Eloquent code.
return "response";
}
}
Document.php
<?php namespace Modules\Documents\Entities;
use Illuminate\Database\Eloquent\Model;
class Document extends Model {
protected $fillable = [];
}
routes.php
$this->app->bind(
'Modules\Documents\Repositories\DocumentRepositoryInterface', function(){
return new Modules\Documents\Repositories\DatabaseDocumentRepository(new Modules\Documents\Entities\Document());
});
Route::group(['prefix' => 'documents', 'namespace' => 'Modules\Documents\Http\Controllers'], function(){
Route::post('/', ['as' => '/', 'uses'=> 'DocumentsController#create']);
});
DocumentsController.php
<?php namespace Modules\Documents\Http\Controllers;
use Modules\Documents\Repositories\DocumentRepositoryInterface;
use Pingpong\Modules\Routing\Controller;
use Module;
use Illuminate\Http\Response;
use Illuminate\Http\Request;
class DocumentsController extends Controller {
private $documents;
public function __construct(DocumentRepositoryInterface $doc){
$this->documents = $doc;
}
public function create(Request $request){
$this->documents->create([]);
return response("", Response::HTTP_CREATED);
}
}
I want to say it has something to do with the mocked object not getting injected into the DocumentsController because the create() function is getting called. I put a print_r in the create function and it displayed in my console. This is strange and it could also be related to PingPong Sky Modules. What am I doing wrong or not doing?
Sorry for the English, but I am using the google translator.
First of all I leave my code:
FtpServiceProdiver.php
<?php namespace Jaimemse\Ftp;
use Illuminate\Support\ServiceProvider;
class FtpServiceProvider extends ServiceProvider {
protected $defer = false;
public function boot()
{
$this->package('jaimemse/ftp');
}
public function register()
{
$this->app->bind('ftp', function()
{
return new Ftp;
});
}
public function provides()
{
return array();
}
}
Ftp.php (the class)
<?php namespace Jaimemse\Ftp;
class Ftp {
public function hello()
{
return 'hola';
}
}
Facades/Ftp.php (Facade)
<?php namespace Jaimemse\Ftp\Facades;
use Illuminate\Support\Facades\Facade;
class Ftp extends Facade {
protected static function getFacadeAccessor() { return 'ftp'; }
}
app.php
'Jaimemse\Ftp\FtpServiceProvider',
'Ftp' => 'Jaimemse\Ftp\Facades\Ftp',
If instead of that Facade put this, if it works:
'Ftp' => 'Jaimemse\Ftp\Ftp',
The problem I have is that when using the alias in the file app.php seeks Ftp class in the folder Facades/Ftp.php
Call to undefined method Jaimemse\Ftp\Facades\Ftp::hello()
Someone can help me? Thanks!
You have to extend the BaseController:
<?php namespace Jaimemse\Ftp;
class Ftp extends \BaseController {
public function hello()
{
return 'hola';
}
}
Also your route should be (with namespace):
Route::get('/ftp', 'Jaimemse\Ftp\Ftp#hello');
Also
use Illuminate\Support\Facades\Facade;
use Illuminate\Support\ServiceProvider;
should be
use \Illuminate\Support\Facades\Facade;
use \Illuminate\Support\ServiceProvider;
You should put in app.php
'Jaimemse\Ftp\FtpServiceProvider', in 'providers' array (before 'aliases')
and in 'aliases' array
'Ftp' => 'Jaimemse\Ftp\Facades\Ftp',
I fixed it by adding in register method:
FtpServiceProvider.php
public function register()
{
$this->app->bind('ftp', function()
{
return new Ftp;
});
$this->app->booting(function()
{
$loader = \Illuminate\Foundation\AliasLoader::getInstance();
$loader->alias('Ftp', 'Jaimemse\Ftp\Ftp');
});
}
Ftp.php
class Ftp {
public function hello()
{
return 'hello';
}
}
App.php
'Jaimemse\Ftp\FtpServiceProvider',
I have not added any app.php alias in the file. I deleted Facade file.
Now I can do things like:
Ftp::hello();
Hope that helps someone. Thank you!