How to implement owen-it/laravel-auditing in a model - php

I want to create an audit trail in my model. I already installed owen-it/laravel-auditing package via Composer. My question is that how can I implement it in my Model or controller. Please see my code for controller and Model below. Thanks
My Controller :
<?php
namespace App\Http\Controllers;
use App\Events\Test;
use App\Letter;
use App\Notifications\LetterNotification;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Validator;
class LetterController extends Controller
{
public function viewLetter()
{
return view('welcome');
}
/**
* Saves email into database
*
* #param array $data
* #return Letter
*/
protected function create(array $data)
{
$letter = Letter::create([
'email' => $data['email']
]);
$this->letterNotify($letter);
return $letter;
}
/**
* Validates email
*/
public function createLetter(Request $request)
{
$this->validate($request,[
'email' => 'required|email|max:255|unique:letters'
],[
'email.required' => 'Email is required.',
'email.unique' => 'Already registered.',
'email.email' => 'Please put a valid Email address'
]);
$this->create($request->all());
return redirect('/')->with('info','You are now registered.');
}
protected function letterNotify($letter)
{`enter code here`
Notification::send($letter, new LetterNotification($letter));
}
}
For my Model:
<?php
namespace App;
use OwenIt\Auditing\Auditable;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
class Letter extends Model implements AuditableContract
{
use Notifiable;
use Auditable;
protected $fillable = ['email'];
protected $table = 'letters';
}

Like I stated in my comment, the Laravel Auditing package only triggers an audit on a database operation involving an Eloquent model and event (by default, created, updated, deleted, restored).
Having said that, here's a list of steps to create an audit when logging in/out:
Create a listener for the Illuminate\Auth\Events\Login event;
Once fired, update a column in the users table that keeps track of the latest login date/time (latest_login_at, for example);
(Optional) update a column with the previous login date/time (last_login_at, for example);
By doing those updates to the users table, the Auditor kicks in;
You can also listen for the OwenIt\Auditing\Events\Auditing or OwenIt\Auditing\Events\Audited events and apply more logic if needed;
Follow the same steps for the Illuminate\Auth\Events\Logout event;

Related

REST principle of PUT method in Laravel

I am preparing a common and current CRUD-type REST API with the users model that laravel brings by default
<?php
namespace App\Models;
use Laravel\Sanctum\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;
class User extends Authenticatable implements MustVerifyEmail {
use HasApiTokens, HasFactory, Notifiable, CanResetPassword;
/**
* The attributes that are mass assignable.
*
* #var string[]
*/
protected $fillable = [
'name',
'email',
'password'
];
/**
* The attributes that should be hidden for serialization.
*
* #var array
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* #var array
*/
protected $casts = [
'email_verified_at' => 'datetime'
];
}
and the routes I generate with Route::apiResource
Route::apiResource('users', UserController);
Since I want to find the user by the email or the id I make a link or explicit injection of the route parameter in the RouteServiceProvider inside the method boot with Route::bind
Route::bind('user', function($userId){
$user = User::where(fn($query) => $query->where('id', $userId)->orWhere('email', $userId))->first();
if(request()->method() === 'PUT') return $user;
return $userId;
});
This is where the main topic comes in, this method (Route::bind ()) should return the instance of a class, if not, it will return an ExceptionModelNotFound 404 - not found, but I would like to be able to receive the null in my controller and thus validate depending on the method (PUT or PATCH) whether to create the new record (which according to REST principles should be able to do a resource if there is no match) or just update an existing one.
My update method of UserController is as follows
public function update(Request $request, User $user){
$input = $request->all();
$method = $request->method();
if($method === 'PUT') $request->validate($this->rules);
$update = [
'PUT' => fn() => ($user) ? $user->update($input) : $user = $user::create($input),
'PATCH' => function() use($user, $input){
foreach ($input as $key => $val){
$user->$key = (!empty($val)) ? $val : $user->$key;
}
$user->save();
return $user;
}
];
return response()->json($update[$method]());
}
Previously I had it without model injection and the method ùpdate worked, something like this
public function update(Request $request, $user){
But due to the explicit injection this no longer takes the parameter, besides that I am only interested in this behavior when updating since this in the rest of the methods helps in the reduction of code and it is fine to handle the 404 - Not found.
I don't know if I'm failing at something or maybe laravel provides a better way to do it which I don't know.
Thanks in advance.
PS: The code is a bit abstracted, it looks a bit messy because I tried to simplify it.
You are not making it easier on yourself by creating a single controller action for both updating and creating users. I have never done it this way, and always define the REST routes like so:
PUT /user/1 = UPDATE
POST /user = CREATE
You should know on the frontend whether you are making a new user or updating an existing one (simply check for an id element for instance).
Also, your current route bind is not the best logic, since that $user query is always executed, even when it is not a PUT request. Nevertheless that code can be simplified much further if you just firstOrFail() that user since you should be separating the functionality into 2 routes anyway.
Personally I never use PATCH, since my PUT routes allow partial updates as well (e.g. only sending an email property update).

How should I correctly construct my unit test in Laravel

I am quite new to unit testing and could do with a bit of guidance. I am trying to write a simple unit test using the Factory pattern in Laravel and phpunit for an application that allows you to add Companies to a database.
I have a Company Model Company.php, a Factory class which uses it CompanyFactory.php, and finally the unit test itself CompaniesTest.php.
Models/Company.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Company extends Model
{
use HasFactory;
protected $table = 'companies';
protected $fillable = [
'name',
'email'
];
}
Database/Factories/CompanyFactory.php
namespace Database\Factories;
use App\Models\Company;
use Illuminate\Database\Eloquent\Factories\Factory;
class CompanyFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* #var string
*/
protected $model = Company::class;
/**
* Define the model's default state.
*
* #return array
*/
public function definition()
{
return [
'name' => $this->faker->name,
'email' => $this->faker->email,
'created_at' => now(),
'updated_at' => now(),
];
}
}
Tests/Feature/CompaniesTest.php
namespace Tests\Unit;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use App\Models\Company;
use Tests\TestCase;
use Illuminate\Support\Str;
use Illuminate\Foundation\Testing\WithFaker;
class CompanyTest extends TestCase
{
use WithFaker, DatabaseTransactions;
public function createCompany($name = NULL)
{
if ($name == NULL) $name = Str::random(6);
$company = Company::factory()->create([
'name' => 'TestName_'.$name
]);
return $company->id;
}
/** #test */
public function company_can_be_created()
{
$name = Str::random(6);
//Create a company
$company_id = $this->createCompany($name);
//Check whether the company name exists in the database
$this->assertDatabaseHas('companies', [
'name' => 'TestName_'.$name
]);
}
}
The test seems to work, but it feels like I might have over-complicated it and probably not followed the correct conventions.
What would be a better way to structure it?
The test looks ok, but what are you actually testing? It seems to me that this test is testing the framework's code, which is actually not what you should do.
Don't test a factory, use it to prepare the data needed before each test. And then run your actual code which you want to test, and assert results.
Update: Continue with CompanyVerifier (see comments). Suppose company can be valid and non-valid. Valid companies can be verified. Then a test may look like:
/** #test */
public function test_valid_company_can_be_verified()
{
// here use a CompanyFactory with some pre-defined data to create "valid" company
$validCompany = $this->createValidCompany();
// here goes the actual code of SUT (system under test)
$verifier = new CompanyVerifier();
$result = $verifier->verify($validCompany);
// here check results
$this->assertTrue($result);
}
The good practice for testing is named AAA (arrange-act-assert). Here the creation of a company with some state is an "arrange" stage. Running tested code is "act". And assert is "assert".
Factories are just a helper for "arrange" stage.

Laravel Notification Store Additional ID Fields

So I have Laravel Notifications setup and it's working perfectly fine.
However, I've extend the migration to include an additional id field:
$table->integer('project_id')->unsigned()->nullable()->index();
Thing is, I don't see how I can actually set that project_id field. My notification looks like this:
<?php
namespace App\Notifications\Project;
use App\Models\Project;
use App\Notifications\Notification;
class ReadyNotification extends Notification
{
protected $project;
public function __construct(Project $project)
{
$this->project = $project;
}
public function toArray($notifiable)
{
return [
'project_id' => $this->project->id,
'name' => $this->project->full_name,
'updated_at' => $this->project->updated_at,
'action' => 'project-ready'
];
}
}
So ya, I can store it in the data, but what if I want to clear the notification specifically by "project" instead of by "user" or by "notification".
For instance if they delete the project, I want the notifications for it cleared, but there is no way to access that unless I do some wild card search on the data column.
So is there anyway to insert that project_id in the notification ?
You could create an Observer to update the field automatically.
NotificationObserver.php
namespace App\Observers;
class NotificationObserver
{
public function creating($notification)
{
$notification->project_id = $notification->data['project_id'] ?? 0;
}
}
EventServiceProvider.php
use App\Observers\NotificationObserver;
use Illuminate\Notifications\DatabaseNotification;
class EventServiceProvider extends ServiceProvider
{
public function boot()
{
parent::boot();
DatabaseNotification::observe(NotificationObserver::class);
}
}
And you should be able to access the table using the default model to perform actions.
DatabaseNotification::where('project_id', 11)->delete();

How to Control Events from firing in Laravel

I have an Event that fires off a Welcome email whenever someone registers a new account on my website.
My problem is, I needed to create a "Create User" page on my admin section. Now every time I create a user from the admin section the email still fires off an email welcoming the new user.
This would be fine, but I need that email to say something else.
I don't want the Welcome email to fire when creating a user from the admin panel.
How can I control this Event from sending the email?
Code pretty much goes in this order:
1. Event code: NewUser.php
namespace App\Events;
... irrelevant classes
use Illuminate\Foundation\Events\Dispatchable;
use App\User;
class NewUser
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $user;
public function __construct(User $user)
{
$this->user = $user;
}
}
2. Listener: SendWelcomeEmail.php
namespace App\Listeners;
use App\Events\NewUser;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Mail;
use App\Mail\NewUserWelcome;
class SendWelcomeEmail
{
public function __construct()
{
//
}
public function handle(NewUser $event)
{
Mail::to($event->user->email)->send(new NewUserWelcome($event->user));
}
}
3. Mail: NewUserWelcome.php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\User;
class NewUserWelcome extends Mailable
{
use Queueable, SerializesModels;
public $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function build()
{
return $this->subject('Welcome To The Website')->markdown('emails.user.newuserwelcome');
}
}
4. Markdown email would be next.
#component('mail::message')
# Greetings bla bla bla
5. EventServiceProvider: is making the call like this:
protected $listen = [
'App\Events\NewUser' => [
'App\Listeners\SendWelcomeEmail',
],
];
6. User model I have the following relevant code:
class User extends Authenticatable {
use Notifiable;
protected $dispatchesEvents = [
'created' => Events\NewUser::class
];
In my ADMIN SAVE USER FUNCTION | UserController This is what I'm using to SAVE the New User From Admin Panel: (No Event classes)
use Illuminate\Http\Request;
use App\User;
use Illuminate\Support\Facades\Hash;
class UserController extends Controller
{
public function adminUserStore(Request $request){
$newsupporter = User::create([
'name'=> $request->name,
'email' => $request->email,
'password' => Hash::make($quickpass),
]);
return back()->with('success','The user has been created and a password reset email has been sent to them.');
}
Any help would be appreciated, I've been battling this one for quite some time.
You can try by adding a new nullable column for your user model, which would check if the user was added by admin or someone else;
$newsupporter = User::create([
'name'=> $request->name,
'email' => $request->email,
'password' => Hash::make($quickpass),
'added_by' => 'admin',
]);
And then create a check and send email only when the user was not added my admin,
public function handle(NewUser $event)
{
if(!$event->user->added_by == 'admin'){
Mail::to($event->user->email)->send(new NewUserWelcome($event->user));
}
}
Personally I would introduce two events instead of the one. Something along the lines of AccountCreatedByUser and AccountCreatedByAdmin. These events can then be handled by separate listeners, which will send separate e-mails. This will mean you'll have to fire these events manually instead of depending on the built-in created event. This can be done like so:
event(new AccountCreatedByUser($user));
Documentation for firing events manually can be found here

Laravel 5 - why is input empty when returning with errors?

I have a form that submits to a controller, which validates the data. If the validation fails it redirects back with the input and the errors. This is the method that deals with the form submission:
<?php namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\User;
class UserController extends Controller {
/**
* Create a new user.
*
* #param Reqeust $request
*
* #return Void
*/
public function postCreate(Request $request)
{
$user = new User;
$rules = $user->rules();
$rules['password'] = 'required|confirmed|min:8';
$v = \Validator::make($request->except('_token', 'roles'), $rules);
if ($v->fails())
{
return redirect()->back()->withInput($request->except('_token', 'password', 'password_confirmation'))->withErrors($v);
}
$user->fill($request->except('_token', 'password', 'password_confirmation'));
$user->password = \Hash::make($request->input('password'));
$user->save();
return redirect()->route('webmanAccounts')->with('messages', [['text' => 'User account created', 'class' => 'alert-success']]);
}
On the page that displays the form I check to see if name, one of the fields, is present and if so populate a User object with the data. The problem is input is always empty.
<?php namespace BackEnd;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Request as RequestFacade;
use App\Http\Controllers\Controller;
use App\Models\Role;
use App\Models\User;
class UserController extends Controller {
public function __construct(Request $request)
{
if ( ! $request->user()->can('accounts'))
{
return abort(403, 'You do not have permission to access this page.');
}
}
/**
* Display the create new user form and process any error messages.
*
* #param Reqeust $request
*
* #return View
*/
public function create(Request $request)
{
$user = new User;
dump(RequestFacade::all());
if (RequestFacade::has('name'))
{
$user->fill(RequestFacade::except('_token', 'roles'));
foreach (RequestFacade::only('roles') as $role)
{
$user->roles()->add($role);
}
}
return view('backend.user.create', ['title' => 'Website Manager :: Create New Account', 'user' => $user, 'roles' => Role::all()]);
}
I have tried RequestFacade, $request and Input, all show as empty. Why isn't the data being passed back?
To add to the strangeness of this, I have another project that uses almost identical code and that works perfectly fine. Why would it work fine for one project but not for another!?
When you use the withInput() method, the data is flashed to the session as "old" data.
$request->old() should give you an array of all the "old" data.
$request->old('name') should give you the "old" name data.

Categories