Best place to store an array in Laravel - laravel

I'm using Laravel 5.2 and my app has the need for select boxes with half-hourly times in various different views throughout.
Here's an example of how the array data looks:
[
'00:00' => '00:00',
'00:30' => '00:30',
'01:00' => '01:00',
/* ... etc ... */
'18:00' => '18:00',
'18:30' => '18:30',
'19:00' => '19:00',
/* ... etc ... */
]
Obviously I don't want to put this in each controller that calls each view, but I'm wondering what the best way of storing it would be. So far, I've thought of the following:
1) A helper function that returns the array
2) A config file in app/config that contains the array
3) A database model (seems excessive)
4) A function that generates the list each time (performance concern?)
Can anyone think of a better way or suggest which one of the ways above would be best and why?

I have found a very good solution to this on Laracasts at https://laracasts.com/series/build-project-flyer-with-me/episodes/5
Here are the steps:
1) Create a file app/Http/Utilities.php:
<?php
namespace App\Http\Utilities;
class Time
{
protected static $times = [
'00:00' => '00:00', '00:30' => '00:30', '01:00' => '01:00'
/** etc ... **/
'22:30' => '22:30', '23:00' => '23:00', '23:30' => '23:30'
];
public static function all()
{
return static::$times;
}
2) In the view, add the following at the top:
#inject ('times','App\Http\Utilities\Time')
3) $times::all() can now be used within the view wherever necessary.

You can use Form Macro with Laravel Collective Package
Laravel Collective: https://github.com/LaravelCollective/html
An awesome example you can find https://github.com/rappasoft/laravel-5-boilerplate
See App\Providers\MacroServiceProvider.php

From my understanding of the question, this would be a perfect situation for View Composers, as it seems like you have some data that has to be generated on multiple views and/or in multiple controller methods.
The way I go about doing this:
Start by running php artisan make:provider ComposerProvider in your terminal.
This is what that provider could look like:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class ComposerServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* #return void
*/
public function boot()
{
view()->composer(
[
'view_one'
'view_two'
], 'App\Http\ViewComposers\DashboardComposer'
);
}
/**
* Register the application services.
*
* #return void
*/
public function register()
{
//
}
}
There you can list all the views that need this data.
An example of how a View Composer could look like. I place these in the folder app/Http/ViewComposers, as the namespace suggests.
<?php
namespace App\Http\ViewComposers;
use Illuminate\View\View;
use Illuminate\Users\Repository as UserRepository;
class DashboardComposer
{
public function __construct()
{
}
public function compose(View $view)
{
$the_variable = 'Hello World';
$data = [
'some_very_important_variable' => $the_variable
];
$view->with($data);
}
}

Related

What is the recommended approach to test a model from another database in laravel?

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?

Laravel: Contract file is not instantiable while building Controller

I am trying to add a shopping cart function to my Laravel application. I installed darryldecode/laravelshoppingcart package from GitHub and have been following instructions in these two websites.
TECHPOOL-Create a Shopping Cart with Laravel 6
LARASHOUT-Laravel E-Commerce Application Development – Checkout
I was able to create most of the shopping cart function with the first website but it didn't cover checkouts and placing orders so I found the second website.
The problem is that the contract file is not working. Here is the error I got.
Illuminate\Contracts\Container\BindingResolutionException
Target [App\Contracts\OrderContract] is not instantiable while building [App\Http\Controllers\CheckoutController].
http://localhost:8000/checkout
Where I use the contract file is in the checkout process witch is explained in the second website. I made few changes in the codes so that it will be consistent with the first website but mostly I followed what the website says.
Here are the codes that are mentioned in the error.
OrderContract.php
<?php
namespace App\Contracts;
interface OrderContract
{
public function storeOrderDetails($params);
}
CheckoutController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Contracts\OrderContract;
use App\Http\Controllers\Controller;
class CheckoutController extends Controller
{
protected $orderRepository;
public function __construct(OrderContract $orderRepository)
{
$this->orderRepository = $orderRepository;
}
public function getCheckout()
{
return view('checkout');
}
public function placeOrder(Request $request)
{
// Before storing the order we should implement the
// request validation which I leave it to you
$order = $this->orderRepository->storeOrderDetails($request->all());
dd($order);
}
}
OrderRepository.php
<?php
namespace App\Repositories;
use Cart;
use App\Models\Order;
use App\Product;
use App\Models\OrderItem;
use App\Contracts\OrderContract;
class OrderRepository extends BaseRepository implements OrderContract
{
public function __construct(Order $model)
{
parent::__construct($model);
$this->model = $model;
}
public function storeOrderDetails($params)
{
$order = Order::create([
'order_number' => 'ORD-' . strtoupper(uniqid()),
'status' => 'pending',
'grand_total' => Cart::getSubTotal(),
'item_count' => Cart::getTotalQuantity(),
'table_number' => $params['table_number'],
'name' => $params['name'],
'notes' => $params['notes']
]);
if ($order) {
$items = Cart::getContent();
foreach ($items as $item) {
// A better way will be to bring the product id with the cart items
// you can explore the package documentation to send product id with the cart
$product = Product::where('name', $item->name)->first();
$orderItem = new OrderItem([
'product_id' => $product->id,
'quantity' => $item->quantity,
'price' => $item->getPriceSum()
]);
$order->items()->save($orderItem);
}
}
return $order;
}
}
RepositoryServiceProvider.php
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Contracts\OrderContract;
use App\Repositories\OrderRepository;
class RepositoryServiceProvider extends ServiceProvider
{
protected $repositories = [
OrderContract::class => OrderRepository::class,
];
/**
* Register services.
*
* #return void
*/
public function register()
{
foreach ($this->repositories as $interface => $implementation) {
$this->app->bind($interface, $implementation);
}
}
/**
* Bootstrap services.
*
* #return void
*/
public function boot()
{
//
}
}
I'm not really familiar with the contract concept since I only started learning Laravel recently and I'm completely lost here. Maybe the problem is that I haven't created another file that is necessary or maybe something else.
Any help would be appreciated as I have tried multiple methods with no success.
Thank you in advance.
Yes, this is expected...your contract should point to a Solid class else it's going to fail while trying to resolve it out of the container. So this is what you should do:
Create a class that implements that trait.
Go to your AppServiceProvider and bind it to that contract like this:
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* #return void
*/
public function register()
{
//
$this->app->bind(\App\Contracts\OrderContract::class, App\Repositories\ClassImplementingOrderContract::class);
}
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
//
}
}
this should fix your problem.
This error could caused even if you have forget to add "RepositoryServiceProvider" into "config/app.php" 's serviceProviders array.

Use Auth in AppServiceProvider

I need the ID of the user who is logged in to get a photo in the profile table, here I am trying to use View but only in the index function that gets $profile, I want all files in the view to have $profile
public function index(){
$profil = Profil_user::where('user_id',$auth)->first();
View::share('profil', $profil);
return view('user.index');
}
I have also tried AppServiceProvider but I get an error in the form of a null value if I don't log in, is there a solution to my problem?
public function boot(){
$auth = Auth::user();
dd($auth);
}
exist several way to pass a variable to all views. I explain some ways.
1. use middleware for all routes that you need to pass variable to those:
create middleware (I named it RootMiddleware)
php artisan make:middleware RootMiddleware
go to app/Http/Middleware/RootMiddleware.php and do following example code:
public function handle($request, Closure $next) {
if(auth()->check()) {
$authUser = auth()->user();
$profil = Profil_user::where('user_id',$authUser->id)->first();
view()->share([
'profil', $profil
]);
}
return $next($request);
}
then must register this middleware in app/Http/Kernel.php and put this line 'root' => RootMiddleware::class, to protected $routeMiddleware array.
then use this middleware of routes or routes group, for example:
Route::group(['middleware' => 'root'], function (){
// your routes that need to $profil, of course it can be used for all routers(because in handle function in RootMiddleware you set if
});
or set for single root:
Route::get('/profile', 'ProfileController#profile')->name('profile')->middleware('RootMiddleware');
2. other way that you pass variable to all views with view composer
go to app/Http and create Composers folder and inside it create ProfileComposer.php, inside ProfileComposer.php like this:
<?php
namespace App\Http\View\Composers;
use Illuminate\View\View;
class ProfileComposer
{
public function __construct()
{
}
public function compose(View $view)
{
$profil = Profil_user::where('user_id', auth()->id)->first();
$view->with([
'profil' => $profil
]);
}
}
now it's time create your service provider class, I named it ComposerServiceProvider
write this command in terminal : php artisan make:provider ComposerServiceProvider
after get Provider created successfully. message go to config/app.php and register your provider with put this \App\Providers\ComposerServiceProvider::class to providers array.
now go to app/Providers/ComposerServiceProvider.php and do like following:
namespace App\Providers;
use App\Http\View\Composers\ProfileComposer;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
class ComposerServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* #return void
*/
public function boot()
{
View::composer(
'*' , ProfileComposer::class // is better in your case use write your views that want to send $profil variable to those
);
/* for certain some view */
//View::composer(
// ['profile', 'dashboard'] , ProfileComposer::class
//);
/* for single view */
//View::composer(
// 'app.user.profile' , ProfileComposer::class
//);
}
/**
* Register the application services.
*
* #return void
*/
public function register()
{
}
}
3. is possible that without create a service provider share your variable in AppServiceProvider, go to app/Provider/AppServiceProvider.php and do as follows:
// Using class based composers...
View::composer(
'profile', 'App\Http\View\Composers\ProfileComposer'
);
// Using Closure based composers...
View::composer('dashboard', function ($view) {
//
});
I hope be useful
you can use this
view()->composer('*', function($view)
{
if (Auth::check()) {
$view->with('currentUser', Auth::user());
}else {
$view->with('currentUser', null);
}
});

How to change hard coded Eloquent $connection only in phpunit tests?

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)

Is it a good practice to add custom method on Laravel Model class to insert record in another table?

I am following a tutorial to create a referal system in Laravel. In the tutorial it was not shown how to implement the addCredit() method of the user model class. I am a bit confuse. Assuming I have another table to keep the record of credits like :
user_credits
------------
user_id
credits
Is it good practice to write the code on user model's addCredits method to update the user_credits table? What will be the best in this case?
class User extends Authenticatable
{
/**
* Add bonus to the user
*/
public function addCredits($credit) {
//
}
}
The listener class to handle addition of the bonus for both the users.
namespace App\Listeners;
use App\Events\UserReferred;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
class RewardUser
{
/**
* Create the event listener.
*
* #return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* #param UserReferred $event
* #return void
*/
public function handle(UserReferred $event)
{
$referral = \App\ReferralLink::find($event->referralId);
if (!is_null($referral)) {
\App\ReferralRelationship::create(['referral_link_id' => $referral->id, 'user_id' => $event->user->id]);
if ($referral->program->name === 'Sign-up Bonus') {
// User who was sharing link
$provider = $referral->user;
// add credits to provider
$provider->addCredits(15);
// User who used the link
$user = $event->user;
$user->addCredits(20);
}
}
}
}
I'm not pretty sure, is it good practice or not, but i prefer abstract such things into a standalone service.
In your case it would be something like that:
CreditService
namespace App\Services;
use App\User;
class CreditService
{
private $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function addCredits($credits)
{
$this->user->credits += $credits;
$this->user->save();
}
}
Then in controller/listener you can work with this service
use App\Services\CreditService;
...
public function handle(UserReferred $event)
{
$referral = \App\ReferralLink::find($event->referralId);
if ( !is_null($referral) ) {
\App\ReferralRelationship::create([
'referral_link_id' => $referral->id,
'user_id' => $event->user->id,
]);
if ( $referral->program->name === 'Sign-up Bonus' ) {
(new CreditService($referral->user))->addCredits(15);
(new CreditService($event->user))->addCredits(20);
}
}
}
The way how you make and then use service might be different. So, if you don't want work via constructors, you can write static class and pass User into method directly.
I often put some additional actions into services. For example, fire events when i need to do it. Or log some things.

Resources