Simple file upload validate request with Symfony - php

Validate an input field of HTML form is a simple operation, as follows:
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Constraints as Assert;
public function adminPassword(Request $request)
{
$this->parameters = $request->request->all();
...
$new_password = $this->parameters['new_password'];
$validator = Validation::createValidator();
$violations = $validator->validate($new_password, [
new Assert\Length([
'min' => 4
])
]);
if (0 !== count($violations)) {
...
}
...
}
Can validation request of HTML form file upload (image), can be done by Symfony in the same simple way?
public function logoUpload(Request $request)
{
$file = $request->files->get('logo');
...
}
The requirement is not using Twig, or Symfony 'Form' ('createFormBuilder'), as not done above.

In Symfony, the result of $request->files->get('key') is an UploadedFile or null.
With an UploadedFile you can use your validator with a file constraint as the example below :
use Symfony\Component\Validator\Constraints\File;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\Validation;
...
public function validateFile(Request $request): ConstraintViolationListInterface
{
$fileConstraints = new File([
'maxSize' => '64M',
'maxSizeMessage' => 'The file is too big',
'mimeTypes' => ['pdf' => 'application/pdf'],
'mimeTypesMessage' => 'The format is incorrect, only PDF allowed'
]);
$validator = Validation::createValidator();
return $validator->validate($request->files->get('key'), $fileConstraints);
}
The method returns an iterator of constraints.
Please note to use MimeTypes you need to install symfony/mime on your app

Related

Laravel broadcasting channel function won't fire

So I'm gonna use laravel broadcasting for a chat app,
I followed Laravel Broadcasting approach,
Uncommented App\Providers\BroadcastServiceProvider
from providers array inside config/app.php
Registered in pusher website, made a channel
and filled the fields below inside .env file with my pusher channel info
PUSHER_APP_ID
PUSHER_APP_KEY
PUSHER_APP_SECRET
PUSHER_APP_CLUSTER
Inside my broadcast.php config file where I set the default driver to pusher, I also added
'options' => [
'cluster' => 'us2',
'encrypted' => true,
],
to pusher array inside connections array based on my channel info in pusher panel
Installed pusher php package on my laravel project using composer require pusher/pusher-php-server "~3.0" command
Here is my event class
<?php
namespace App\Events;
use App\User;
use App\TherapyMessage;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use App\AppLog;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
class TherapyMessageSent implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* User that sent the message
*
* #var User
*/
public $user;
/**
* Message details
*
* #var Message
*/
public $message;
/**
* Create a new event instance.
*
* #return void
*/
public function __construct(User $user, TherapyMessage $message)
{
$this->user = $user;
$this->message = $message;
}
/**
* Get the channels the event should broadcast on.
*
* #return Channel|array
*/
public function broadcastOn()
{
$message_id = $this->message->id;
$user = $this->user;
AppLog::create([
'file_name' => __FILE__,
'message' => "broadcast before send with Message ID of $message_id from $user->full_name"
]);
return new PrivateChannel("therapy-chat.$message_id");
}
}
The AppLog is a model that I use for logging inside the project
I tried implementing ShouldBroadcast interface at first but that didn't work either
I also registered my event inside EventServiceProvider.php file and run php artisan event:generate command, here is the EventServiceProvider $listen array:
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
TherapyMessageSent::class
],
];
I also imported the event namespace next to other namespaces inside the file:
use \App\Events\TherapyMessageSent;
Here is the channel that I defined inside routes/channels.php file:
use App\AppLog;
Broadcast::channel('therapy-chat.{message_id}', function ($user, $message_id) {
AppLog::create([
'file_name' => __FILE__,
'message' => "broadcast sending with Message ID of $message_id to $user->full_name"
]);
if (!Auth::check()) return false;
$message = \App\TherapyMessage::find($message_id);
if (!$message) {
AppLog::create([
'file_name' => __FILE__,
'message' => "Message with ID of $message_id Not Found for broadcasting"
]);
return false;
}
$will_send = false;
if ($therapist = $user->therapist) {
$will_send = $therapist->id === $message->therapist_id;
} else if ($patient = $user->patient) {
$will_send = $message->patient_id === $patient->id;
}
if ($will_send) {
AppLog::create([
'file_name' => __FILE__,
'message' => "Message with ID of $message_id broadcasted to $user->full_name"
]);
}
return $will_send;
});
Finally, this is my controller method:
public function sendToTherapist(Request $request) {
$validation = \Validator::make($request->all(), ['message' => 'required']);
if ($validation->fails()) return $this->validationError($validation);
$user = \Auth::user();
$patient = $user->patient;
$therapist = $patient->therapist;
if (!$therapist) return $this->errorWithMessage('Patient does not have Therapist');
$message = \App\TherapyMessage::create([
'patient_id' => $patient->id,
'therapist_id' => $therapist->id,
'type' => TherapyMessageType::TEXT,
'text' => $request->message,
'sender_role' => TherapyMessageSenderRole::PATIENT
]);
broadcast(new TherapyMessageSent($user, $message))->toOthers();
return $this->success(['id' => $message->id]);
}
My controller class extends from BaseController which is a custom controller class with helper methods such as success(), validationError() and errorWithMessage()
As you see in the code above
I filled $user and $message with correct values and the request works without any error
I think the channel method won't even be fired,
as I check the AppLog table when I call broadcast method, only the log inside TherapyMessageSent event broadcastOn function is saved
and even the log that I save at the beginning of channels.php method, isn't saved so I think this method is never executed.
If anyone could help me with the problem, I'd be thankful.

Correct way to seed and delete data in the database for Laravel testing?

I am new to Laravel tests, and i am currently building my tests so they use certain data in my database to check if an HTTP request did the appropriate job.
I am trying to figure out how i can "seed" data into my database before i run my tests but also delete this data after all the tests are complete (either succeed of failed, should get deleted anyway).
I tried to understand how to do it correctly reading some articles in the internet but just couldn't find the right solution for me.
I know that in node.js the Mocha tests has a "beforeEach" to manipulate data before every test, is there a similar option in PHP Laravel?
With laravel version greater than 5.5 you can use the RefreshDatabase trait within your test which will reset your database after test have been run. All you will have to do is to add it at the top of your test like bellow
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
class YourModelNameTest extends TestCase
{
use RefreshDatabase;
public function setUp()
{
parent::setUp();
// This will run all your migration
Artisan::call('migrate');
// This will seed your database
Artisan::call('db:seed');
// If you wan to seed a specific table
Artisan::call('db:seed', ['--class' => 'TableNameSeeder ', '--database' => 'testing']);
}
}
The RefreshDatabase trait have define refreshDatabase which migrate the database before and after each test. you can see the code here
RefreshDatabse refreshDatabase
While Yves answer should work (separate database), i have found a set of methods that can help achieving what i needed:
"setUp" and "tearDown".
the "setUp" method will do something before the set of tests will run (the test class) and "tearDown" will do something after all of those tests were executed.
I'm attaching my test class here for an example of how i used those:
<?php
namespace Tests\Feature;
use App\User;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
class UserTest extends TestCase
{
private $token;
public function setUp(){
parent::setUp();
$userData = [
'email' => 'mytestemail#email.com',
'password' => '123456'
];
$response = json_decode($this->post('/register', $userData)->baseResponse->getContent());
$this->token = $response->token;
}
/**
* A basic test example.
*
* #return void
*/
public function testRegister()
{
$validData = [
'email' => 'testrerere#email.com',
'password' => '123456'
];
$invalidEmail = [
'email' => 'not_a_real_email',
'password' => '123456'
];
$invalidPassword = [
'email' => 'test2#email.com',
'password' => '12'
];
$emptyData = [];
$goodResponse = $this->post('/register', $validData);
$goodResponse->assertStatus(201);
$goodResponse->assertJsonStructure(['token']);
User::where('email', 'testrerere#email.com')->delete();
$invalidEmailResponse = $this->post('/register', $invalidEmail);
$invalidEmailResponse->assertStatus(400);
$invalidPasswordResponse = $this->post('/register', $invalidPassword);
$invalidPasswordResponse->assertStatus(400);
$emptyDataResponse = $this->post('/register', $emptyData);
$emptyDataResponse->assertStatus(400);
}
public function testToken()
{
$validData = [
'email' => 'mytestemail#email.com',
'password' => '123456'
];
$invalidData = [
'email' => 'nonexistingemail#test.com',
'password' => '123456'
];
$validDataResponse = $this->post('/token', $validData);
$validDataResponse->assertStatus(200);
$validDataResponse->assertJsonStructure(['token']);
$invalidDataResponse = $this->post('/token', $invalidData);
$invalidDataResponse->assertStatus(400);
}
//get an account object based on a token
public function testAccount()
{
$goodResponse = $this->withHeaders([
'Authorization' => 'Bearer ' . $this->token,
])->json('GET', '/account');
$goodResponse
->assertStatus(200)
->assertJsonStructure([
'user',
]);
//altering the token to get an invalid token error
$badResponse = $this->withHeaders([
'Authorization' => 'Bearer L' . $this->token,
])->json('GET', '/account');
// print_r($badResponse->baseResponse->getContent());
$badResponse->assertJson(['status' => 'Token is Invalid']);
}
public function tearDown()
{
User::where('email', 'mytestemail#email.com')->delete();
User::where('email', 'testrerere#email.com')->delete();
parent::tearDown();
}
}

Laravel validation always returns 200 OK from API

I want to separate my validator with my controller, but API always responds with a 200 OK in Postman.
Request validation:
class PostRequest extends FormRequest
{
use Types;
public function authorize()
{
return true;
}
public function rules()
{
// $auth = $this->request->user();
return [
'name' => 'required|string|max:255',
'country_id' => 'required|exists:countries,id',
'city' => 'required|max:255',
'phone_no' => 'required|regex:/[0-9]{10,13}/',
'occupation' => 'required|max:255'
];
}
}
SenderController:
public function store(PostRequest $request)
{
$auth = $request->user();
$sender = Sender::create([
'name' => $request->name,
'country_id' => $request->country_id,
'city' => $request->city,
'phone_no' => $request->phone_no,
'occupation' => $request->occupation
]);
return new Resource($sender);
}
When I'm sending a request without name, it will return a response with a status of 200. I want to display $validator->errors() in my response when I forget to input name. How can I do that?
Route and call:
Route::post('sender', 'V1\SenderController#store');
POST: localhost:8000/api/v1/sender
You should make sure you're sending the request with the Accept: application/json header.
Without that - Laravel won't detect that it's an API request, and won't generate the 422 response with errors.
If you are requesting from postman then always check header and set
Accept = 'application/json'
The proper way to display validation errors using injected FormRequest object is to override failedValidation.
As stated previously in comment section, injected FormRequest object validates the request by itself, no need to validate it a second time in the controller body (that's one of the main benefit of it). If the validation fails, you won't hit the controller body anyway.
Create an abstract class that extends FormRequest and override failedValidation.
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\Response;
abstract class BaseFormRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
abstract public function rules(): array;
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
abstract public function authorize(): bool;
/**
* Handle a failed validation attempt.
*
* #param Validator $validator
*
* #return void
*/
protected function failedValidation(Validator $validator): void
{
$errors = $validator->errors();
throw new HttpResponseException(response()->json([
'errors' => $errors
], Response::HTTP_UNPROCESSABLE_ENTITY));
}
}
You can now extends BaseFormRequest on all your APIs request validation classes. Validation errors will now be displayed as a JSON encoded message with a Unprocessable Entity (422) HTTP code.
Source : https://medium.com/#Sirolad/laravel-5-5-api-form-request-validation-errors-d49a85cd29f2
I solved the problem with adding more code in my PostRequest.php
public $validator = null;
protected function failedValidation($validator)
{
$this->validator = $validator;
}
and I show the error message by using controller
if (isset($request->validator) && $request->validator->fails()) {
return response()->json([
'error_code'=> 'VALIDATION_ERROR',
'message' => 'The given data was invalid.',
'errors' => $request->validator->errors()
]);
}
Your Postman response is sending back the welcome.blade.php default view. This means that it isn't hitting your controller method, as the method returns the Resource instance. This means that the validator won't be run.
Check that the route you are making the request to is definitely the correct route. Also check that you are making a POST request instead of a GET request.
you have to validate your request before saving
public function store(PostRequest $request)
{
$auth = $request->validate();
if($auth->fails())
{
return response()->json($auth->errors());
}
$sender = Sender::create([
'name' => $request->name,
'country_id' => $request->country_id,
'city' => $request->city,
'phone_no' => $request->phone_no,
'occupation' => $request->occupation
]);
return new Resource($sender);
}

How to test file upload with laravel and phpunit?

I'm trying to run this functional test on my laravel controller. I would like to test image processing, but to do so I want to fake image uploading. How do I do this? I found a few examples online but none seem to work for me. Here's what I have:
public function testResizeMethod()
{
$this->prepareCleanDB();
$this->_createAccessableCompany();
$local_file = __DIR__ . '/test-files/large-avatar.jpg';
$uploadedFile = new Symfony\Component\HttpFoundation\File\UploadedFile(
$local_file,
'large-avatar.jpg',
'image/jpeg',
null,
null,
true
);
$values = array(
'company_id' => $this->company->id
);
$response = $this->action(
'POST',
'FileStorageController#store',
$values,
['file' => $uploadedFile]
);
$readable_response = $this->getReadableResponseObject($response);
}
But the controller doesn't get passed this check:
elseif (!Input::hasFile('file'))
{
return Response::error('No file uploaded');
}
So somehow the file isn't passed correctly. How do I go about this?
For anyone else stumbling upon this question, you can nowadays do this:
$response = $this->postJson('/product-import', [
'file' => new \Illuminate\Http\UploadedFile(resource_path('test-files/large-avatar.jpg'), 'large-avatar.jpg', null, null, null, true),
]);
UPDATE
In Laravel 6 the constructor of \Illuminate\Http\UploadedFile Class has 5 parameters instead of 6. This is the new constructor:
/**
* #param string $path The full temporary path to the file
* #param string $originalName The original file name of the uploaded file
* #param string|null $mimeType The type of the file as provided by PHP; null defaults to application/octet-stream
* #param int|null $error The error constant of the upload (one of PHP's UPLOAD_ERR_XXX constants); null defaults to UPLOAD_ERR_OK
* #param bool $test Whether the test mode is active
* Local files are used in test mode hence the code should not enforce HTTP uploads
*
* #throws FileException If file_uploads is disabled
* #throws FileNotFoundException If the file does not exist
*/
public function __construct(string $path, string $originalName, string $mimeType = null, int $error = null, $test = false)
{
// ...
}
So the above solution becomes simply:
$response = $this->postJson('/product-import', [
'file' => new \Illuminate\Http\UploadedFile(resource_path('test-files/large-avatar.jpg'), 'large-avatar.jpg', null, null, true),
]);
It works for me.
Docs for CrawlerTrait.html#method_action reads:
Parameters
string $method
string $action
array $wildcards
array $parameters
array $cookies
array $files
array $server
string $content
So I assume the correct call should be
$response = $this->action(
'POST',
'FileStorageController#store',
[],
$values,
[],
['file' => $uploadedFile]
);
unless it requires non-empty wildcards and cookies.
The best and Easiest way : First Import the Necessary things
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
Then make a fake file to upload.
Storage::fake('local');
$file = UploadedFile::fake()->create('file.pdf');
Then make a JSON Data to pass the file. Example
$parameters =[
'institute'=>'Allen Peter Institute',
'total_marks'=>'100',
'aggregate_marks'=>'78',
'percentage'=>'78',
'year'=>'2002',
'qualification_document'=>$file,
];
Then send the Data to your API.
$user = User::where('email','candidate#fakemail.com')->first();
$response = $this->json('post', 'api/user', $parameters, $this->headers($user));
$response->assertStatus(200);
I hope it will work.
With phpunit you can attach a file to a form by using attach() method.
Example from lumen docs:
public function testPhotoCanBeUploaded()
{
$this->visit('/upload')
->name('File Name', 'name')
->attach($absolutePathToFile, 'photo')
->press('Upload')
->see('Upload Successful!');
}
Here is a full example how to test with custom files. I needed this for parsing CSV files with known format so my files had to had exact formatting and contents. If you need just images or random sized files use $file->fake->image() or create() methods. Those come bundled with Laravel.
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
class PanelistImportTest extends TestCase
{
/** #test */
public function user_should_be_able_to_upload_csv_file()
{
// If your route requires authenticated user
$user = Factory('App\User')->create();
$this->actingAs($user);
// Fake any disk here
Storage::fake('local');
$filePath='/tmp/randomstring.csv';
// Create file
file_put_contents($filePath, "HeaderA,HeaderB,HeaderC\n");
$this->postJson('/upload', [
'file' => new UploadedFile($filePath,'test.csv', null, null, null, true),
])->assertStatus(200);
Storage::disk('local')->assertExists('test.csv');
}
}
Here is the controller to go with it:
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Storage;
class UploadController extends Controller
{
public function save(Request $request)
{
$file = $request->file('file');
Storage::disk('local')->putFileAs('', $file, $file->getClientOriginalName());
return response([
'message' => 'uploaded'
], 200);
}
}
Add similar setUp() method into your testcase:
protected function setUp()
{
parent::setUp();
$_FILES = array(
'image' => array(
'name' => 'test.jpg',
'tmp_name' => __DIR__ . '/_files/phpunit-test.jpg',
'type' => 'image/jpeg',
'size' => 499,
'error' => 0
)
);
}
This will spoof your $_FILES global and let Laravel think that there is something uploaded.

Laravel 5.1 PHP

I'm new to coding and Laravel 5.1, and after watching the tutorials by Laracasts I have been creating my own webpage. I came across and error that I cant fix...
Method [send] does not exist.
My code looks like this:
namespace App\Http\Controllers;
use Mail;
use App\User;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class ContactController extends Controller
{
/**
*
* #param Request $request
*/
public function emailContactForm (Request $request){
$msg = $request->input('message');
$name = $request->input('name');
$email = $request->input('email');
//
$this->validate($request, [
'title' => 'required|max 500',
'name' => 'required',
'email' => 'required',
]);
//
Mail::send(
'emails.contactForm',
[
'message'=>$msg,
'name'=>$name,
],
function($m) use ($email) {
$m->to('jessica.blake#autumndev.co.uk', 'say hi')
->subject('new message')
->from($email);
}
);
//
return;
}
}
I'm trying to use the mail function, which we have now got working, but the send still doesn't? Any suggestions? Thanks!
EDIT: Full stack trace as per laravel log file: http://pastebin.com/ZLiQ7Wgu
At the very first sight, you are calling the controller method send() but you actually named it emailContactForm()
You dont post routes and actions so the quick fix by now is trying to rename emailContactForm to send, despite instead you should probably need to review all your related routing logic.

Categories