Laravel CSV import - can I use the request validation? - php

I am using Laravel 5.6 and have setup a form request validation for my form which submits a single row, validates it and then adds to the database. This all works fine.
For batch import of multiple rows I have a CSV import. The CSV is parsed into an array, and each line of the array contains exactly the same type of data as provided in my form. Therefore it can use the same validation rules.
I am a little lost how to actually implement this; the data parsed from the CSV is in an array, and not the request object that the form validation request is looking for.
Does anyone have any tips on the best way to be able to use my form validation for both the form and CSV without duplicating code?
EDIT
If anyone is interested, my final solution was to not use the form request validation. In my case, it was easier to add the validation rules and messages to some protected functions inside the controller. This means that they can be re-used across each of the controller functions that need it (store, csvStore etc.) without duplicate code. In this case, I am not sure what advantages the form request validation feature gives.
//reformat CSV into array
$master = [];
$line_id = 1;
foreach ($data as $row) {
//skip blank rows
if (empty($row['sku'])) continue;
//build master
foreach($row as $key => $value){
if(!empty($value)) $master[$row['sku']][$key] = $row[$key];
}
//add line number for debugging
$master[$row['sku']]['line_number'] = $line_id;
$line_id++;
}
//Validate each row of CSV individually
$error_messages = new MessageBag();
$error_count = 0;
$duplicate_count = 0;
if(empty($master)){
//empty $master
$error_messages->add('', 'CSV file does not contain valid data or is empty');
flash()->message('Nothing was imported');
return redirect()->back()->withErrors($error_messages);
} else {
foreach($master as $row){
$validator = Validator::make($row,$this->createValidationRules(), $this->createValidationMessages());
//Check validation
if ($validator->fails()){
$master[$row['sku']]['valid'] = false;
if(isset($validator->failed()['sku']['Unique'])){
$duplicate_count ++;
if(!request('ignore-duplicates') && !request('ignore-errors')) $error_messages->merge($validator->errors()->messages()); //save error messages
} else {
$error_count ++;
if(!request('ignore-errors')) $error_messages->merge($validator->errors()->messages()); //save error messages
}
} else {
$master[$row['sku']]['valid'] = true;
}
}
}
//add successful rows to DB
$success_count = 0;
foreach($master as $row){
if($row['valid'] == true){
$productCreate = new ProductCreate();
$productCreate->create($row);
$success_count++;
}
}
I then used the success/error/duplicate counts to send back a suitable error message bag and/or flash messages.

You could approach it by creating a Request object macro to turn the CSV into an array, then use middleware to parse an incoming request if it's a csv file and merge it into the incoming request. Then your application's validation can validate it using array validation.
Start by making the service provider to house your request macro:
php artisan make:provider RequestMacroParseCsvProvider
Then in the service provider:
Add this at the top to pull in the Request class:
use Illuminate\Http\Request;
Inside the register method of the provider:
Request::macro('parseCsv', function ($fileNameKey) {
// Note: while working inside of the request macro closure, you can access the request object by referencing $this->{key_of_request_item}
// You will be running your parser against $fileNameKey which will be the key of the request file coming in. So you'd access it like:
if ($this->hasFile($fileNameKey)) {
// Your code to parse the csv would go here. Instantiate your csv parsing class or whatever...
// $file = $this->file($fileNameKey);
// Store the parsed csv in an array, maybe named $parsedCsv?
}
return empty($parsedCsv) ? [] : $parsedCsv;
});
Register the service provider in your config/app.php
App\Providers\RequestMacroParseCsvProvider::class,
Create a middleware to check if the incoming request contains a csv
php artisan make:middleware MergeCsvArrayIntoRequest
In the handle method:
if ($request->has('your_csv_request_key')) {
$parsedCsv = $request->parseCsv('your_csv_request_key');
// Then add it into the request with a key of 'parsedCsv' or whatever you want to call it
$request->merge(['parsedCsv' => $parsedCsv]);
}
return $next($request);
Register your middleware in your app/Http/Kernel.php:
protected $middleware = [
...
\App\Http\Middleware\MergeCsvArrayIntoRequest::class,
...
];
Or put it into $routeMiddleware if you don't want it to be global.
'parse.csv' => \App\Http\Middleware\MergeCsvArrayIntoRequest::class,
Now, your middleware is intercepting and converting any CSV files you upload and you can validate parsedCsv request key using Laravel's array validation.
You can definitely make some improvements to make it more flexible, if you want. I've done something similar, but not quite file related, in another project where I needed to modify the request before it got to my controller's validation and it worked.
Hope this helps.

Related

Alternative to $form->isValid() for use in a Symfony DataMapper?

I am building a Symfony data transformer class called EventDataMapper. It handles two fields: A TextType field called My mapDataToForms() definition looks like this:
public function mapDataToForms($data, $forms)
{
$existingTitle = $data->getTitle();
$existingAttendees = $data->getAttendees();
$this->propertyPathMapper->mapDataToForms($data, $forms);
foreach ($forms as $index => $form) {
if ($form->getName() === 'title' && !is_null($existingTitle)) {
$form->setData($existingTitle);
}
if ($form->getName() === 'attendees' && !is_null($existingAttendees)) {
$form->setData($existingAttendees);
}
}
}
The problem is that I'm setting data before validation runs, so if I submit a form with a non-numeric string in the "attendees" field, I get an ugly TransformationFailedException ('Unable to transform value for property path "attendees": Expected a numeric'). And if I try to do a check for whether my field is valid by adding a call to $form->isValid() in the line before I call $form->setData(), I get a LogicException. ('Cannot check if an unsubmitted form is valid. Call Form::isSubmitted() before Form::isValid().')
Is there any way for my to preemptively call a validator on this specific field from within my DataMapper?
(Yes, this can be somewhat prevented with frontend logic. But I don't want to rely too much on that.)
Closing the loop on this. Here's what we did.
A colleague made a new form type corresponding to a new adapter class that wraps our two previous classes, providing a uniform set of wrapper methods for interacting with them.
We passed Symfony's validator service into our new form type using the constructor.
In that form type, we're using $builder->addEventListener() to add a callback/listener on the POST_SUBMIT event. Here's the callback:
function(FormEvent $event): void {
$adapter = $event->getData();
$form = $event->getForm();
$errors = $adapter->propagate($this->validator);
foreach ($errors as $error) {
$formError = new FormError($error->getMessage());
$targetPath = self::mapPropertyPath($error->getPropertyPath());
$target = $targetPath !== null ? $form->get($targetPath) : $form;
$target->addError($formError);
}
}
The adapter, in turn, has some logic that does various translations of data into a form that can be used in our legacy classes, followed by this:
return $validator->validate($this->legacyObject);
This works well for us. I hope it helps somebody else out too.

Design pattern that handles multiple steps

So I have a complicated onboarding process that does several steps. I created a class that handles the process but I've added a few more steps and I'd like to refactor this into something a bit more manageable. I refactored to use Laravel's pipeline, but feel this may not be the best refactor due to the output needing to be modified before each step.
Here is an example before and after with some pseudo code.
before
class OnboardingClass {
public $user;
public $conversation;
public function create($firstName, $lastName, $email){
// Step 1
$user = User::create();
// Step 2
$conversation = Conversation::create(); // store information for new user + existing user
// Step 3
$conversation->messages()->create(); // store a message on the conversation
// Step 4
// Send api request to analytics
// Step 5
// Send api request to other service
return $this;
}
}
after
class OnboardingClass{
public $user;
public $conversation;
public function create($firstName, $lastName, $email){
$data = ['first_name' => $firstName, ...]; // form data
$pipeline = app(Pipeline::Class);
$pipeline->send($data)
->through([
CreateUser::class,
CreateNewUserConversation::class,
AddWelcomeMessageToConversation::class,
...
])->then(function($data){
// set all properties returned from last class in pipeline.
$this->user = $data['user'];
$this->conversation = $data['conversation'];
});
return $this;
}
}
Now within each class I modify the previous data and output a modified version something like this
class CreateUser implements Pipe {
public function handle($data, Closure $next) {
// do some stuff
$user = User::create():
return $next([
'user' => $user,
'other' => 'something else'
]);
}
}
In my controller I am simply calling the create method.
class someController() {
public function store($request){
$onboarding = app(OnboardingClass::class);
$onboarding->create('John', 'Doe', 'john#example.com');
}
}
So the first pipe receives the raw form fields and outputs what the second pipe needs to get the job done in its class, then the next class outputs the data required by the next class, so on and so forth. The data that comes into each pipe is not the same each time and you cannot modify the order.
Feels a bit weird and I'm sure there is a cleaner way to handle this.
Any design pattern I can utilize to clean this up a bit?
I think you could try using Laravel Service Provider, for example, you could build a login service provider; or Event & Listener, for example, you could build an listener for login and triggers a event to handle all the necessary logics. Can't really tell which one is the best since outcome is the same and it makes same amount of network requests, but it's more on personal preferences

API Request $_POST returning empty array

I'm using Zurmo and trying to create a new account using REST API. I followed this documentation precisely: http://zurmo.org/wiki/rest-api-specification-accounts to pass the required parameters as json array.
This is my php code:
public function actionCreateOrUpdate()
{
$params=$_POST;
$modelClassName=$this->getModelName();
foreach ($params as $param)
{
if (!isset($param))
{
$message = Zurmo::t('ZurmoModule', 'Please provide data.');
throw new ApiException($message);
}
$r=$this->GetParam($param);
$res= array('status' => 'SUCCESS', 'data' => array($r));
print_r(json_encode($res,true));
}
}
function GetParam ($param){
$modelClassName=$this->getModelName();
if (isset($param['mobile_id'] ) && !$param['mobile_id']=='' &&!$param['mobile_id']==null)
{ $id=$param['mobile_id'];
$params=array();
foreach ($param as $k => $v) {
if(!($k=='mobile_id')) {
$params[$k] = $v;}
}
if ($params=null){$message = Zurmo::t('ZurmoModule', 'Please provide data.');
throw new ApiException($message);}
$tableName = $modelClassName::getTableName();
$beans = ZurmoRedBean::find($tableName, "mobile_id = '$id'");
if (count($beans) > 0)
{
$result = $this->processUpdate($id, $params);
}else{
$result = $this->processCreate($params,$id);
}
}
return $result;
}
The problem is that the $_POST method is returning an empty array. While debugging I tried to use print_r($_POST) and it also returned an empty array. I also tried to pass parameters as plain text and got the same result. I tried $_GET method and it worked. I think the problem is in the $_POST method, maybe I need to change something in my .php files. Any ideas please?
You should first hit the api with static data, to check if it works fine, then try to integrate php within that static data. You will need to read the documentation for which action accepts which format, and which method is supported(GET OR POST). Then try die(); , before sending if the array formed is as per the documentation.
I had similar issue when creating Account using REST API from java client. The problem was I did not send the proper POST request.
Another symptom also was on server side print_r(file_get_contents("php://input"),true); php code returned the correct request data.
Specifically the root cause was:
The Content-Type HTTP header was not "application/x-www-form-urlencoded"
The value field value in POST request was not properly encoded ( I used java.net.URLEncoder.encode method to overcome this)
After fixing these it worked.

Possible to modify / write to a custom parameters.yml file in symfony2?

apologies for the possible n00b question but here we go. I'm currently writing a service class in symfony2 which collects data using ajax. The data basically consists of two timestamps sent upon form submit. What I then want to do is pass this to my controller and write it to a custom parameters.yml file so I can store the values in this file and update this file each time a user submits the form. I am getting an error like this :
Impossible to call set() on a frozen ParameterBag
And some searching on Google tells me that I cannot modify the Container once it has been compiled. The line in particular which is causing this is :
$this->container->setParameter('quicksign.start.off', $startOff);
Okay time to show my code. Here is my controller :
public function updateServiceSigAction() {
$logger = $this->get('logger');
$request = $this->get('request');
$errors = array();
if (WebserviceController::POST_ONLY && $request->getMethod() != 'POST') {
$errors[] = "Not allowed !";
return $this->sendResponse($errors);
}
$params = $request->request->all();
if (count($params) == 0) {
$errors[] = "Missing parameters !";
return $this->sendResponse($errors);
} else {
$servicesig_services = $this->get('servicesigservice');
$errors = $servicesig_services->updateServiceSig($params, false);
}
return $this->sendResponse($errors, array(), true);
}
And here is the relevant method of my service class :
public function updateServiceSig($params, $need_to_flush = true) {
$errors = array();
$startOff = $params['date_debut'];
$endOff = $params['date_fin'];
if (empty($startOff) || empty($endOff)) {
$errors[] = "Missing parameters from query !";
} else {
$this->container->setParameter('quicksign.start.off', $startOff);
$this->container->setParameter('quicksign.end.off', $endOff);
}
return $errors;
}
Maybe I should do this before compiling the container ? But I don't know where exactly the container is being compiled...
Or maybe I should do it another way...?
So here's how I got it done :
use Symfony\Component\Yaml\Dumper; //I'm includng the yml dumper. Then :
$ymlDump = array( 'parameters' => array(
'quicksign.active' => 'On',
'quicksign.start.off' => $startOff,
'quicksign.end.off' => $endOff ),
);
$dumper = new Dumper();
$yaml = $dumper->dump($ymlDump);
$path = WEB_DIRECTORY . '/../app/config/parameters.sig.yml';
file_put_contents($path, $yaml);
Where WEB_DIRECTORY is defined in the app.php file -> however, you should use
%kernel.root_dir%
in the service configuration.
From my understanding you are using the parameters.yml file wrong. The official documentation states:
One use for this is to inject the values into your services. This allows you to configure different versions of services between applications or multiple services based on the same class but configured differently within a single application.
So the file is not for storing a services state but to configure the initial state. You use it if multiple applications use the same source-code. An example would be a staging and a production environment, or multiple services in one application like two ORMs that need different connection parameters. With that said you should probably use an entity to store your timestamps in it.
If you really need a file you can use e.g. Symfony's YAML component to manage a custom .yml file.

Zend Action Controller - refactoring strategy

I've built a first-run web service on Zend Framework (1.10), and now I'm looking at ways to refactor some of the logic in my Action Controllers so that it will be easier for me and the rest of my team to expand and maintain the service.
I can see where there are opportunities for refactoring, but I'm not clear on the best strategies on how. The best documentation and tutorials on controllers only talk about small scale applications, and don't really discuss how to abstract the more repetitive code that creeps into larger scales.
The basic structure for our action controllers are:
Extract XML message from the request body - This includes validation against an action-specific relaxNG schema
Prepare the XML response
Validate the data in the request message (invalid data throws an exception - a message is added to the response which is sent immediately)
Perform database action (select/insert/update/delete)
Return success or failure of action, with required information
A simple example is this action which returns a list of vendors based on a flexible set of criteria:
class Api_VendorController extends Lib_Controller_Action
{
public function getDetailsAction()
{
try {
$request = new Lib_XML_Request('1.0');
$request->load($this->getRequest()->getRawBody(), dirname(__FILE__) . '/../resources/xml/relaxng/vendor/getDetails.xml');
} catch (Lib_XML_Request_Exception $e) {
// Log exception, if logger available
if ($log = $this->getLog()) {
$log->warn('API/Vendor/getDetails: Error validating incoming request message', $e);
}
// Elevate as general error
throw new Zend_Controller_Action_Exception($e->getMessage(), 400);
}
$response = new Lib_XML_Response('API/vendor/getDetails');
try {
$criteria = array();
$fields = $request->getElementsByTagName('field');
for ($i = 0; $i < $fields->length; $i++) {
$name = trim($fields->item($i)->attributes->getNamedItem('name')->nodeValue);
if (!isset($criteria[$name])) {
$criteria[$name] = array();
}
$criteria[$name][] = trim($fields->item($i)->childNodes->item(0)->nodeValue);
}
$vendors = $this->_mappers['vendor']->find($criteria);
if (count($vendors) < 1) {
throw new Api_VendorController_Exception('Could not find any vendors matching your criteria');
}
$response->append('success');
foreach ($vendors as $vendor) {
$v = $vendor->toArray();
$response->append('vendor', $v);
}
} catch (Api_VendorController_Exception $e) {
// Send failure message
$error = $response->append('error');
$response->appendChild($error, 'message', $e->getMessage());
// Log exception, if logger available
if ($log = $this->getLog()) {
$log->warn('API/Account/GetDetails: ' . $e->getMessage(), $e);
}
}
echo $response->save();
}
}
So - knowing where the commonalities are in my controllers, what's the best strategy for refactoring while keeping it Zend-like and also testable with PHPUnit?
I did think about abstracting more of the controller logic into a parent class (Lib_Controller_Action), but this makes unit testing more complicated in a way that seems to me to be wrong.
Two ideas (just creating an answer from the comments above):
Push commonality down into service/repository classes? Such classes would be testable, would be usable across controllers, and could make controller code more compact.
Gather commonality into action helpers.
Since you have to do this step every time a request is made, you could store that receive, parse and validate the received request in a Zend_Controller_Plugin which would be run every PreDispatch of all controllers. (Only do-able if your XML request are standardized) (If you use XMLRPC, REST or some standard way to build requests to your service, you could look forward those modules built in ZF)
The validation of the data (action specific) could be done in a controller method (which would then be called by the action(s) needing it) (if your parametters are specific to one or many actions of that controller) or you could do it with the patterns Factory and Builder in the case that you have a lot of shared params between controllers/actions
// call to the factory
$filteredRequest = My_Param_Factory::factory($controller, $action, $paramsArray) // call the right builder based on the action/controller combination
// the actual Factory
class My_Param_Factory
{
public static function factory($controller, $action, $params)
{
$builderClass = "My_Param_Builder_" . ucfirst($controller) . '_' . ucfirst($action);
$builder = new $builderClass($params);
return $builder->build();
}
}
Your builder would then call specific parameters validating classes based on that specific builder needs (which would improve re-usability)
In your controller, if every required params are valid, you pass the processing to the right method of the right model
$userModel->getUserInfo($id) // for example
Which would remove all of the dataprocessing operations from the controllers which would only have to check if input is ok and then dispatch accordingly.
Store the results (error or succes) in a variable that will be sent to the view
Process the data (format and escape (replace < with < if they are to be included in the response for example)), send to a view helper to build XML then print (echo) the data in the view (which will be the response for your user).
public function getDetailsAction()
{
if ($areRequestParamsValid === true) {
// process data
} else {
// Build specific error message (or call action helper or controller method if error is general)
}
$this->view->data = $result
}

Categories