I have an Eloquent Model like this:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class SomeModel extends Model
{
protected $connection = 'global_connection';
......................
The problem is that this $connection has to be hard coded because I have a multi tenant web platform and all the tenants should read from this Database.
But when now in tests I am hitting the Controller route store() and I don't have access to the model!
I just do this:
public function store()
{
SomeModel::create($request->validated());
return response()->json(['msg' => 'Success']);
}
Which works great when using it as a user through browser...
But now I want to somehow force that model NOT to use that hard coded $connection and set it to Testing database connection...
And this is my Test
/** #test */
public function user_can_create_some_model(): void
{
$attributes = [
'name' => 'Some Name',
'title' => 'Some Title',
];
$response = $this->postJson($this->route, $attributes)->assertSuccessful();
}
Is there any way to achieve this with some Laravel magic maybe :)?
Because you asked for Laravel magic... Here it goes. Probably an overkill and over engineered way.
Let's first create an interface whose sole purpose is to define a function that returns a connection string.
app/Connection.php
namespace App;
interface Connection
{
public function getConnection();
}
Then let's create a concrete implementation that we can use in real world (production).
app/GlobalConnection.php
namespace App;
class GlobalConnection implements Connection
{
public function getConnection()
{
return 'global-connection';
}
}
And also another implementation we can use in our tests.
app/TestingConnection.php (you can also put this in your tests directory, but make sure to change the namespace to the appropriate one)
namespace App;
class TestingConnection implements Connection
{
public function getConnection()
{
return 'testing-connection';
}
}
Now let's go ahead and tell Laravel which concrete implementation we want to use by default. This can be done by going to the app/Providers/AppServiceProvider.php file and adding this bit in the register method.
app/Providers/AppServiceProvider.php
namespace App\Providers;
use App\Connection;
use App\GlobalConnection;
// ...
public function register()
{
// ...
$this->app->bind(Connection::class, GlobalConnection::class);
// ...
}
Let's use it in our model.
app/SomeModel.php
namespace App;
use Illuminate\Database\Eloquent\Model;
class SomeModel extends Model
{
public function __construct(Connection $connection, $attributes = [])
{
parent::__construct($attributes);
$this->connection = $connection->getConnection();
}
// ...
}
Almost there. Now in our tests, we can replace the GlobalConnection implementation with the TestingConnection implementation. Here is how.
tests/Feature/ExampleTest.php
namespace Tests\Feature;
use Tests\TestCase;
use App\Connection;
use App\TestingConnection;
class ExampleTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
$this->app->instance(Connection::class, TestingConnection::class);
}
/** #test */
public function your_test()
{
// $connection is 'testing-connection' in here
}
}
Code is untested, but should work. You can also create a facade to access the method statically then use Mockery to mock the method call and return a desired connection string while in testing.
Unfortunately for me, none of these answers didn't do the trick because of my specific DB setup for multi tenancy. I had a little help and this is the right solution for this problem:
Create a custom class ConnectionResolver somewhere under tests/ directory in laravel
<?php
namespace Tests;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\ConnectionResolver as IlluminateConnectionResolver;
class ConnectionResolver extends IlluminateConnectionResolver
{
protected $original;
protected $name;
public function __construct(ConnectionResolverInterface $original, string $name)
{
$this->original = $original;
$this->name = $name;
}
public function connection($name = null)
{
return $this->original->connection($this->name);
}
public function getDefaultConnection()
{
return $this->name;
}
}
In test use it like this
create a method called create() inside tests/TestCase.php
protected function create($attributes = [], $model = '', $route = '')
{
$this->withoutExceptionHandling();
$original = $model::getConnectionResolver();
$model::setConnectionResolver(new ConnectionResolver($original, 'testing'));
$response = $this->postJson($route, $attributes)->assertSuccessful();
$model = new $model;
$this->assertDatabaseHas('testing_db.'.$model->getTable(), $attributes);
$model::setConnectionResolver($original);
return $response;
}
and in actual test you can simply do this:
/** #test */
public function user_can_create_model(): void
{
$attributes = [
'name' => 'Test Name',
'title' => 'Test Title',
'description' => 'Test Description',
];
$model = Model::class;
$route = 'model_store_route';
$this->create($attributes, $model, $route);
}
Note: that test method can have only one line when using setUp() method and $this-> notation
And that's it. What this does is forcing the custom connection name (which should be written inside config/database.php) and the model during that call will work with that connection no matter what you specify inside the model, therefore it will store the data into DB which you have specified in $model::setConnectionResolver(new ConnectionResolver($original, 'HERE'));
This is tested for Laravel 8 & 9 and Super Simple.
Here is an example of switching the connection while testing.
In your model ->
class YourModel extends Model {
protected $connection = 'remote';
public function __construct(array $attributes = [])
{
if(config('app.env') === 'testing') {
$this->connection = 'sqlite';
}
parent::__construct($attributes);
}
}
In the Eloquent Model you have the following method.
/**
* Set the connection associated with the model.
*
* #param string|null $name
* #return $this
*/
public function setConnection($name)
{
$this->connection = $name;
return $this;
}
So you can just do
$user = new User();
$user->setConnection('connectionName')
One option would be to create a new environment file just for testing, that way you can overwrite the connection credentials only for your tests and you would not have to touch your models:
tests/CreatesApplication.php
public function createApplication()
{
$app = require __DIR__ . '/../bootstrap/app.php';
$app->loadEnvironmentFrom('.env.testing'); // add this
$app->make(Kernel::class)->bootstrap();
return $app;
}
Copy your .env file to .env.testing and change your database credentials for the connection global_connection to your test database credentials.
I am not sure how you configured your connection but it probably looks something like the following.
database.php
'global_connection' => [
'database' => env('DB_GLOBAL_DATABASE', ''),
'username' => env('DB_GLOBAL_USERNAME', ''),
'password' => env('DB_GLOBAL_PASSWORD', ''),
],
.env.testing:
DB_GLOBAL_DATABASE=database
DB_GLOBAL_USERNAME=username
DB_GLOBAL_PASSWORD=secret
Now you can use the global_connection connection but it will use your test database.
Additionally you could then remove all environment values from the phpunit.xml file and move them into the .env.testing file so you have all environment values for your tests in one place.
If you don't want to create a new environment file you could of course just update the values in your phpunit.xml file:
<php>
<server name="DB_GLOBAL_DATABASE" value="database"/>
<server name="DB_GLOBAL_USERNAME" value="username"/>
<server name="DB_GLOBAL_PASSWORD" value="password"/>
</php>
The most "magical" thing I suggest you could do is focus exclusively on the test and try to not modify the model at all:
/** #test */
public function user_can_create_some_model(): void
{
config([ "database.connections.global_connection" => [
'driver' => 'mysql', 'host' => x // basically override everything that is in config/database.php
]);
$attributes = [
'name' => 'Some Name',
'title' => 'Some Title',
];
$response = $this->postJson($this->route, $attributes)->assertSuccessful();
}
Hopefully when the configuration needs to be read the new one will be used.
If your global_connection configuration is read from the .env file you can also override the env variables in your test runner configuration (e.g. phpunit.xml)
Related
I am developping a new application using TDD for the first time.
One of the requirement is to reuse a table that already exists for another project.
There is two database, one for my new app, one for the already existing project.
I have a model created in my new applications that references that other model.
GainCode (new app) -> belongsTo Company (existing app)
My first idea was to create a factory for both of these model for my test, but then I realised that because I set the protected $connection property on my Company model, it actually inserts data in the real table during the tests.
I tried to add the following code in the model to change the connectiong if it is under test environment:
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
if (App::environment('testing')) {
$this->connection = 'sqlite';
}
}
However, I noticed this does not work because I have no migration file for a Company.
I then decided to remove the factory and use Company::first() in my test instead and factory.
Before
'company_id' => function() {
return Company::factory()->create()->ID;
}
After
class GainCodeFactory extends Factory
{
/**
* Define the model's default state.
*
* #return array<string, mixed>
*/
public function definition()
{
return [
'code' => $this->faker->unique()->randomNumber(5),
'description_fr' => $this->faker->sentence(5),
'description_en' => $this->faker->sentence(5),
'inactive' => $this->faker->boolean(),
'company_id' => function() {
return Company::first()->ID;
}
];
}
}
Unit test sample
<?php
namespace Tests\Unit;
use App\Models\Company;
use App\Models\GainCode;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class GainCodeTest extends TestCase
{
use RefreshDatabase;
/** #test */
public function it_belongs_to_a_company()
{
$company = Company::first(); // BEFORE: Company::factory()->create();
$gainCode = GainCode::factory()->create(['company_id' => $company->ID]);
$this->assertEquals($company->ID, $gainCode->company->ID);
}
}
What is the recommended way to do TDD with such requirement?
Should I revert back to using factory and maintain a migration file for that table?
Since I am developing package, so I put my factories to custom path like this:
-- app
-- packages
-----mockizart
-------blog
---------database
--------------factories
----------------- PageModelFactory.php
---------src
this is how i load factory in my service provider (I already make sure the path is correct by clicking it on phpstorm):
function boot()
{
Factory::construct($this->app->make(\Faker\Generator::class), __DIR__."/../database/factories");
}
this is my page model factory (I already made sure this file was really loaded):
<?php
/** #var \Illuminate\Database\Eloquent\Factory $factory */
use Mockizart\Blog\Dodols\PageModel;
use Faker\Generator as Faker;
$factory->define(PageModel::class, function (Faker $faker) {
return [
'name' => "retretre",
'slug' => "retretret",
'type' => 0,
'category' => 0,
'tags' => "",
'content' => "",
];
});
and this is my script test :
use Mockizart\Blog\Dodols\PageModel;
.....
.....
/** #test */
public function edit_page()
{
dd(PageModel::find(1)); <-- this return was NULL so I think my class and namespace does exist.
factory(PageModel::class)->make(); <-- this cause error "unable to locate factory......"
$response = $this->get('/blog/page/edit/15');
$response->assertStatus(200);
}
so if you are using orchestra\Testbench, the correct way to load custom factories is in the setUp() method of your Test class or TestCase NOT in your Service Provider.
the code would be like this:
class TestCase extends \Orchestra\Testbench\TestCase
{
public function setUp(): void
{
parent::setUp();
// additional setup
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
$this->withFactories(__DIR__.'/../database/factories');
}
protected function getPackageProviders($app)
{
return BlogServiceProvider::class;
}
}
My plugin code:
public function boot()
{
**I can:**
$user = new User();
$table = $user->getTable();
**I would like to:**
echo User::getTable();
exit;
$user = $this->user;
\Serviom\Guestpage\Models\Post::extend(function($model) use ($user) {
$model->rules = [
'name' => 'required|between:3,100',
'subject' => 'required|between:3,100',
'desc' => 'required|between:10,1000',
'parent_id' => 'nullable|exists:serviom_guestpage_posts,id',
'user_id' => 'nullable|exists:' . $table . ',id',
Variable in modal $table is protected member so its really nothing we can do about that but we can simply access it by adding public method to class [ extend it ]
In any of your plugin you can add this snippet
use RainLab\User\Models\User as UserModel;
class Plugin extends PluginBase
{
public function boot() {
UserModel::extend(function($model) {
$model->addDynamicMethod('getTableName', function() use ($model) {
return $model->getTable();
});
});
//....
Now you can able to call
echo User::getTableName();
Its like kind of hack but its only if you really badly needed it then you do something like this
if any doubt please comment.
You can add a static method to your Model as a helper :
class MyModel extends Model
{
public static function getTableName()
{
return with(new static)->getTable();
}
}
Usage : MyModel::getTableName()
Source
I'm migrating from Laravel 4 to 5.7 and having trouble with my custom auth provider. I've followed various walkthroughs (e.g. 1, 2, 3) as well as quite a bit of googling.
I've attempted to get this working by the following:
Set the guards and providers and link to my target model.
'defaults' => [
'guard' => 'custom_auth_guard',
'passwords' => 'users',
],
'guards' => [
'custom_auth_guard' => [
'driver' => 'session',
'provider' => 'custom_auth_provider',
],
],
'providers' => [
'custom_auth_provider' => [
'driver' => 'custom',
'model' => App\UserAccount::class,
],
],
Register the driver defined in the above provider. I'm piggybacking off AuthServiceProvider for ease
...
public function boot()
{
$this->registerPolicies();
\Auth::provider('custom',function() {
return new App\Auth\CustomUserProvider;
});
}
...
Created my custom provider which has my retrieveByCredentials, etc. I've replaced the logic with some die() to validate if it is making it here. In Laravel 4, it used to go to validateCredentials().
class CustomUserProvider implements UserProviderInterface {
public function __construct()
{
die('__construct');
}
public function retrieveByID($identifier)
{
die('retrieveByID');
}
public function retrieveByCredentials(array $credentials)
{
die('retrieveByCredentials');
}
public function validateCredentials(\Illuminate\Auth\UserInterface $user, array $credentials)
{
die('validateCredentials');
}
For reference, App/UserAccount looks like so
class UserAccount extends Authenticatable
{
use Notifiable;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'public.user_account';
// no updated_at, created_at
public $timestamps = false;
private $_roles = [];
private $_permissions = [];
}
Finally, I am calling it via my controller.
if(\Auth::attempt($credentials){
return \Redirect::intended('/dashboard');
}
I have also tried to call the guard direct
if(\Auth::guard('custom_auth_guard')->attempt($credentials){
return \Redirect::intended('/dashboard');
}
This results in the following error: "Auth guard [custom_auth_guard] is not defined."
I've tried a few other commands to make sure there is no cache issue:
composer update
php artisan cache:clear
The results: when I call Auth::attempt($credentials) Laravel is trying to run a query on the users table. the expected result is that it would hit one of the die()'s in CustomUserProvider... or at lease try and query public.user_account as defined in the model.
I've been messing with this for some time and I must be missing something simple... hopefully someone with a bit more experience in Laravel 5 can see what I am doing wrong.
Thanks in advance!!
Managed to work it out. Couple little problems but the main one was that I was trying to piggyback on AuthServiceProvider as opposed to my own provider. Below is what I did to get a custom auth provider working in Laravel 5.7
Set the provider in config.auth.php.
'providers' => [
'user' => [
'driver' => 'eloquent',
'model' => \UserAccount::class,
],
],
Create a new provider in app/providers/ . This links the listed provider above with the correct User Provider Code.
namespace App\Providers;
use Auth;
use App\Auth\CustomUserProvider;
use Illuminate\Support\ServiceProvider;
class CustomAuthProvider extends ServiceProvider
{
public function register()
{
//
}
public function boot()
{
Auth::provider('eloquent',function()
{
return new CustomUserProvider(new \UserAccount());
});
}
}
Created my custom provider in app/auth/. This is the logic for validating the user and replaces the laravel functions for auth. I had an issue here where it was validating but not populating the user object. I originally had a test to see if the object was null and if it was, populate... however it was always populated with an empty object. removing the test allowed me to call Auth::user() functions.
namespace App\Auth;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
use Illuminate\Auth\EloquentUserProvider;
class CustomUserProvider implements EloquentUserProvider{
public function __construct()
{
$this->user = $user;
}
public function retrieveByID($identifier)
{
$this->user = \UserAccount::find($identifier);
return $this->user;
}
public function retrieveByCredentials(array $credentials)
{
// find user by username
$user = \UserAccount::where('name', $credentials['username'])->first();
// validate
return $user;
}
public function validateCredentials(\Illuminate\Auth\UserInterface $user, array $credentials)
{
//logic to validate user
}
Updated App/Models/UserAccount looks like so
use Illuminate\Foundation\Auth\User as Authenticatable;
class UserAccount extends Authenticatable
{
protected $table = 'public.user_account';
// no updated_at, created_at
public $timestamps = false;
private $_roles = [];
private $_permissions = [];
}
That's it. I can now validate via the below call
if(\Auth::attempt($credentials){
return \Redirect::intended('/dashboard');
}
I'm working with Laravel 5.3 and I'm trying to set a role when someone signs up, I've used the Zizaco Entrust library.
I'm unsure on the best way to achieve something like this.
I tried to do this inside RegisterController's create method like below:
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
$user = User::where('email', '=', $data['email'])->first();
// role attach alias
$user->attachRole($employee);
}
But obviously that's not right. So I'm a bit unsure on what the best practice is with this sort of thing.
If, as your comment on the OP suggests, you always want to assign the same role to a registered user, you can use a Model Observer for this - it's really simple.
// app/Observers/UserObserver.php
<?php namespace App\Observers;
use App\Models\User;
use App\Models\Role; // or the namespace to the Zizaco Role class
class UserObserver {
public function created( User $user ) {
$role = Role::find( 1 ); // or any other way of getting a role
$user->attachRole( $role );
}
Then you simply register the observer in your AppServiceProvider:
// app/Providers/AppServiceProvider.php
use App\Models\User;
use App\Observers\UserObserver;
class AppServiceProvider extends Provider {
public function boot() {
User::observe( new UserObserver );
// ...
}
// ...
}
This answer is mainly based off looking at your current solution, with a dash of original question.
Rather than filling out your model with methods like createNew, you'll probably find things easier to manage if you create a type of class specifically for interacting with models. You can call this a Repository or a Service or whatever takes your fancy, but we'll run with Service.
// app/Services/UserService.php
<?php namespace App\Services;
use App\Models\User; // or wherever your User model is
class UserService {
public function __construct( User $user ) {
$this->user = $user;
}
public function create( array $attributes, $role = null ) {
$user = $this->user->create( $attributes );
if ( $role ) {
$user->attachRole( $role );
}
return $user;
}
}
Now we need to deal with the fact that we've lost the hashing of passwords:
// app/Models/User.php
class User ... {
public function setPasswordAttribute( $password ) {
$this->attributes[ 'password' ] = bcrypt( $password );
}
}
And now we have the problem of sending out an activation email - that can be solved cleanly with events. Run this in the terminal:
php artisan make:event UserHasRegistered
and it should look something like this:
// app/Events/UserHasRegistered.php
<?php namespace App\Events;
use App\Models\User;
use Illuminate\Queue\SerializesModels;
class UserHasRegistered extends Event {
use SerializesModels;
public $user;
public function __construct( User $user ) {
$this->user = $user;
}
}
Now we need a listener for the event:
php artisan make:listener SendUserWelcomeEmail
And this can be as complex as you like, here's one I'm just copy/pasting from a project I have lying around:
// app/Listeners/SendUserWelcomeEmail.php
<?php namespace App\Listeners;
use App\Events\UserHasRegistered;
use App\Services\NotificationService;
class SendUserWelcomeEmail {
protected $notificationService;
public function __construct( NotificationService $notificationService ) {
$this->notify = $notificationService;
}
public function handle( UserHasRegistered $event ) {
$this->notify
->byEmail( $event->user->email, 'Welcome to the site', 'welcome-user' )
->send();
}
}
All that remains is to tell Laravel that the Event and Listener we've just created are related, then to fire the event.
// app/Providers/EventServiceProvider.php
use App\Events\UserHasRegistered;
use App\Listeners\SendUserWelcomeEmail;
class EventServiceProvider extends ServiceProvider {
// find this array near the top, and add this in
protected $listen = [
UserHasRegistered::class => [
SendUserWelcomeEmail::class,
],
];
// ...
}
Now we just need to raise the event - see my other post about Model Observers. First off you'll need to import Event and App\Events\UserHasRegistered, then in your created method, just call Event::fire( new UserHasRegistered( $user ) ).
What I ended up doing, since I do need to do more than just one operation on the user creation is having another function for user creations.
User model
/**
* Create a new user instance after a valid registration.
*
* #param array $attributes
* #param null $role
* #param bool $send_activation_email
*
* #return User $user
*
* #internal param array $args
*/
public function createNew(array $attributes, $role = null, $send_activation_email = true)
{
$this->name = $attributes['name'];
$this->company_id = $attributes['company_id'];
$this->email = $attributes['email'];
$this->password = bcrypt($attributes['password']);
$this->save();
if (isset($role)) {
// Assigning the role to the new user
$this->attachRole($role);
}
//If the activation email flag is ok, we send the email
if ($send_activation_email) {
$this->sendAccountActivationEmail();
}
return $this;
}
and calling it like:
User Controller
$user = new User();
$user->createNew($request->all(), $request->role);
It might not be the best solution, but it does the job, and it's future prof, so if the logic on the user creation grows can be implemented aswell.