Multiple forms for entity instances - symfony3 - php

I'm trying to create a form that would allow a manager to approve a list of time off requests (also planning to have a todo list and want to be able to mark them as done).
I have read [Generate same form type on same page multiple times Symfony2 (as well as several others) and I am close to understanding but I'm fairly to new to Symfony and not clear on what parts of the code should go in what files. I am using a form type and a controller in Symfony3 with Doctrine.
I have list of the entity instances that were returned from a query ($em->createQuery) in the controller and I am looking to produce a form for each entity instance or even two forms per entity (one for approve and one for reject).
The referenced question says you need a loop to display and save them. My intention is to only work on (submit) one at a time. I assume this part of the code would go in the controller?
I am using an indexAction for the controller but using it more like an Edit action since I will be processing forms, so I pass in a Request object and the objects as parameters.
>
class HRMgrController extends Controller
{
/**
* Lists all manager role requests and provide a means to approve/deny.
*
* #Route("/", name="hrmgr_index")
* #Method({"GET", "POST"})
* #Security("has_role('ROLE_APP_MANAGER')")
*/
public function indexAction(Request $request, TimeOffRequest $timeOffRequest)
{
if (!empty($timeOffRequest)) {
$form = $this->createForm('CockpitBundle\Form\TORApprovalType', $timeOffRequest);
print "TOR Id = " . $timeOffRequest->getId() . "<BR>";
$em = $this->getDoctrine()->getManager();
$form->handleRequest($request);
print "Form name = " . $form->getName() . "<BR>";
if ($form->isSubmitted() && $form->isValid()) {
if ($form->get('approve')->isClicked()) {
print "This puppy was approved";
$timeOffRequest['status'] = 4;
}
if ($form->get('reject')->isClicked()) {
print "This puppy was rejected";
$timeOffRequest['status'] = 1;
}
$this->getDoctrine()->getManager()->flush();
print "At least its there<BR>";
// return $this->redirectToRoute('hrmgr_index');
} else {
print "did not detect form submission<BR>";
}
}
$emp = new \CockpitBundle\Entity\Employee();
$date = new \DateTime();
$year = $date->format('Y');
$username = $this->getUser()->getUserName();
$user = $em->getRepository('CockpitBundle:Employee')->findByUsername($username);
$employees = $em->getRepository('CockpitBundle:Employee')->htmlContact($user);
$tors = $em->getRepository('CockpitBundle:TimeOffRequest')->findMgrUnapprovedTORs($user->getId());
$timeoff = "<h3>Unapproved Time Off Requests</h3>";
$actions = true;
$torforms = [];
foreach ($tors as $tor) {
$target = $this->generateUrl('hrmgr_index',array("tor_id" => $tor->getId()));
$torforms[] = $this->actionForm($tor,$target)->createView();
}
return $this->render('hrmgr/index.html.twig', array(
'torforms' => $torforms,
));
I have the forms working nowbut when I submit them the isSubmitted() doesn't seem to be working. It outputs the "did not detect form submission" currently.
So when I have multiple forms and I submit one, does the handleRequest get the right one? I think I might be confusing two concepts here as well. I recently changed the code to submit the ID of the timeOffRequest as a parameter to the route. It is properly picking that up which allows me to potentially update the form but that part of the code doesn't seem to be working.
I noticed that if I look at the debugger, I get something like:
> approval_form_2
[▼
"reject" => ""
"_token" => "IE1rGa5c0vaJYk74_ncxgFsoDU7wWlkAAWWjLe3Jr1w"
]
if I click the reject button. I get a similar form with "approve" if I click the approve button so it seems like I am close. Also, the proper ID shows up from the route given in the action.
Here is the form generator:
<?php
namespace CockpitBundle\Form;
use CockpitBundle\Entity\Employee;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\ButtonType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
class TORApprovalType extends AbstractType
{
private $nameSuffix = null;
private $name = 'time_req_approval';
public function __constructor(string $suffix = null) {
//parent::__construct();
$this->nameSuffix = $this->generateNameSuffix();
}
private function generateNameSuffix() {
if ($this->nameSuffix == null || $this->nameSuffix == '') {
$generator = new SecureRandom();
//change data to alphanumeric string
return bin2hex($generator->nextBytes(10));
}
return $this->nameSuffix;
}
public function setNameSuffix($suffix){
$this->nameSuffix = $suffix;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Build your form...
$builder->add('approve', SubmitType::class, array(
'label' => "Approve",
'attr' => array("class"=>"action-approve"),
));
$builder->add('reject', SubmitType::class, array(
'label' => "Reject",
'attr' => array("class"=>"action-reject"),
));
//$builder->add('employee');
}
public function getName() {
if ($this->nameSuffix == null || $this->nameSuffix == "" ) {
$this->nameSuffix = $this->generateNameSuffix();
}
return $this->name .'_'. $this->nameSuffix;
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'CockpitBundle\Entity\TimeOffRequest'
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'cockpitbundle_timeoffrequest';
}
}
Any clues? (sorry I am on vacation so not particular quick with updates.

You can do multiple submit button: check your formtype
->add('approve', 'submit')
->add('reject', 'submit')
then in your controller
if ($form->isValid()) {
// ... do something
// the save_and_add button was clicked
if ($form->get('approve')->isClicked()) {
// probably redirect to the add page again
}
if ($form->get('reject')->isClicked()) {
// probably redirect to the add page again
}
// redirect to the show page for the just submitted item
}

I was able to get it working with the following builder.
$builder->add('approve', SubmitType::class, array(
'label' => "Approve",
'attr' => array("class"=>"action-approve"),
));
$builder->add('reject', SubmitType::class, array(
'label' => "Reject",
'attr' => array("class"=>"action-reject"),
));
Then in the controller form I generate and process the forms as such. Not sure if it is the optimal way but it works find. Of course, this approach redraws the whole list each time, but that is fine for what I'm doing.
class HRMgrController extends Controller
{
/**
* Lists all manager role requests and provide a means to approve/deny.
*
* #Route("/", name="manager_home")
* #Method({"GET"})
* #Security("has_role('ROLE_APP_MANAGER')")
*/
public function indexAction()
{
$em = $this->getDoctrine()->getManager();
$emp = new \CockpitBundle\Entity\Employee();
$employeeSummary = [];
$date = new \DateTime();
$year = $date->format('Y');
$username = $this->getUser()->getUserName();
$user = $em->getRepository('CockpitBundle:Employee')->findByUsername($username);
$myemployees = $em->getRepository('CockpitBundle:Employee')->findManagersEmployees($user);
$torRep = $em->getRepository('CockpitBundle:TimeOffRequest');
$toas = [];
$torforms = [];
foreach ($myemployees as $employee) {
$tors = $torRep->findAllMine($employee,$year);
$toas[$employee->getDisplayName()] = $em->getRepository('CockpitBundle:TimeOffAllocation')->getEmpAllocation($employee->getId(),$year);
$employeeSummary[$employee->getDisplayName()] = $torRep->mySummary($tors,$toas[$employee->getDisplayName()]);
if (array_key_exists('items',$employeeSummary[$employee->getDisplayName()]['Vacation']['Requested'])) {
foreach ($employeeSummary[$employee->getDisplayName()]['Vacation']['Requested']['items'] as $tor) {
$target = $this->generateUrl('hrmgr_tor_approval',array("tor_id" => $tor->getId()));
$torforms[] = $this->actionForm($tor,$target)->createView();
}
}
if (array_key_exists('items',$employeeSummary[$employee->getDisplayName()]['Sick Time']['Requested'])) {
foreach ($employeeSummary[$employee->getDisplayName()]['Sick Time']['Requested']['items'] as $tor) {
$target = $this->generateUrl('hrmgr_tor_approval',array("tor_id" => $tor->getId()));
$torforms[] = $this->actionForm($tor,$target)->createView();
}
}
if (array_key_exists('Time Off',$employeeSummary[$employee->getDisplayName()]) &&
array_key_exists('items',$employeeSummary[$employee->getDisplayName()]['Time Off']['Requested'])) {
foreach ($employeeSummary[$employee->getDisplayName()]['Time Off']['Requested']['items'] as $tor) {
$target = $this->generateUrl('hrmgr_tor_approval',array("tor_id" => $tor->getId()));
$torforms[] = $this->actionForm($tor,$target)->createView();
}
}
}
return $this->render('hrmgr/index.html.twig', array(
'employeeSummary' => $employeeSummary,
'torforms' => $torforms,
'year' => $year,
));
}
/**
* Lists all manager role requests and provide a means to approve/deny.
*
* #Route("/{tor_id}", name="hrmgr_tor_approval")
* #Method({ "POST" })
* #ParamConverter("timeOffRequest", class="CockpitBundle:TimeOffRequest", options={"id"="tor_id"})
* #Security("has_role('ROLE_APP_MANAGER')")
*/
public function approvalAction(Request $request, TimeOffRequest $timeOffRequest)
{
if (!empty($timeOffRequest)) {
$form = $this->createForm('CockpitBundle\Form\TORApprovalType', $timeOffRequest);
$id = $timeOffRequest->getId();
$em = $this->getDoctrine()->getManager();
$form->handleRequest($request);
$postparams = $request->request->all();
if (array_key_exists("approval_form_$id",$postparams)) {
// Form was submitted
if (array_key_exists("approve",$postparams["approval_form_$id"])) {
$status = $em->getReference('CockpitBundle\Entity\TimeOffStatus', 4);
$timeOffRequest->setStatus($status);
$timeOffRequest->setApprovedDate(new \DateTime);
$em->persist($timeOffRequest);
$em->flush($timeOffRequest);
}
if (array_key_exists("reject",$postparams["approval_form_$id"])) {
$status = $em->getReference('CockpitBundle\Entity\TimeOffStatus', 1);
$timeOffRequest->setStatus($status);
$timeOffRequest->setApprovedDate(new \DateTime);
$em->persist($timeOffRequest);
$em->flush($timeOffRequest);
}
} else {
print "Form did not exist<BR>";
}
return $this->redirectToRoute('manager_home');
}
}
public function actionForm($tor,$target) {
return $this->get('form.factory')->createNamedBuilder('approval_form_'.$tor->getId(), \CockpitBundle\Form\TORApprovalType::class, $tor,
array("action"=> $target))->getForm();
}
}

Related

Symfony3.4: The option "choices" with value [...] is expected to be of type "null" or "array" or "\Traversable", but is of type [...]

In Symfony3.4, when I corresponded to "" deleted in symfony3 or later, the following error occurred.
I don't think I'm using the "" anymore, but it seems to be recognized by symfony.
composer dump-autoload and php bin/console cache:clear have been executed.
Is there anything wrong with it?
https://.com/symfony/symfony/blob/2.7/UPGRADE-2.7.md
Error
The option "choices" with value Symfony\Component\Form\ChoiceList\
is expected to be of type "null" or "array" or "\Traversable", but is of type
"Symfony\Component\Form\ChoiceList\".
StaffChoiceList.php->StaffChoiceLoader.php
// use Symfony\Component\Form\ChoiceList\;
// use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList;
use AppBundle\Model\Service\StaffService;
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
// class StaffChoiceList extends
class StaffChoiceLoader implements ChoiceLoaderInterface
{
private $loginStaff;
private $currentStaff;
/**
* #var StaffService
*/
private $staffService;
public function __construct(StaffService $staffService, $loginStaff)
{
$this->staffService = $staffService;
$this->loginStaff = $loginStaff;
}
public function setCurrentStaff($currentStaff)
{
$this->currentStaff = $currentStaff;
}
// Add"$value = null"
public function ($value = null)
{
// Get the same shop staff as the login staff
$staffs = $this->staffService->getStaffByShop($this->loginStaff->getShop());
// If the current staff is not included in the acquired staff (due to transfer etc.), add it to the end
if ($this->currentStaff && !array_search($this->currentStaff, $staffs)) {
$staffs[] = $this->currentStaff;
}
return new ChoiceList($staffs, $staffs);
}
//Add
public function loadChoicesForValues(array $values, $value = null)
{
// TODO: Implement loadChoicesForValues() method.
}
//Add
public function loadValuesForChoices(array $choices, $value = null)
{
// TODO: Implement loadValuesForChoices() method.
}
}
ArticleType.php
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
use AppBundle\Form\ChoiceList\StaffChoiceLoader;
abstract class ArticleType extends AbstractType
{
// $authorChoiceList = new StaffChoiceList($this->staffService, $options['login_staff']);
//Add
$factory = new DefaultChoiceListFactory();
$authorChoiceList = $factory->createListFromLoader(new StaffChoiceLoader($this->staffService, $options['login_staff']));
$builder->add("author", EntityType::class, array(
"required" => true,
"class" => "AppBundle:Staff",
"choices" => $authorChoiceList,
"placeholder" => "Please select",
));
Tried Code
1. choices -> choice_loader
The following error
The option "choice_loader" with value Symfony\Component\Form\ChoiceList\ is expected to be of type "null" or "Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface", but is of type "Symfony\Component\Form\ChoiceList\".
Change $authorChoiceList
// $factory = new DefaultChoiceListFactory();
// $authorChoiceList = $factory->createListFromLoader(new StaffChoiceLoader($this->staffService, $options['login_staff']));
$authorChoiceList = new StaffChoiceLoader($this->staffService, $options['login_staff']);
The following error
Warning: current() expects parameter 1 to be array, null given
AbstractController->createForm('AppBundle\\Form\\Type\\Article\\BrandEventType', object(BrandEvent), array('method' => 'POST', 'action' => '/app_dev.php/admin/hq/brandevent/', 'login_staff' => object(Staff)))
in src/AppBundle/Controller/BaseArticleController.php (line 186)
));
} else if ($articleType == 'brandevent'){
$form = $this->createForm(BrandEventType::class, $article, array(
"method" => "POST",
"action" => $this->generateUrl($this->createRoute,
array("articleType" => $articleType)),
//Error Code
"login_staff" => $this->getStaff(),
));
}

Yii2 $_GET and Yii::$app->request->get() not working

Background
I'm having an odd issue with a simple line of code that I don't understand. I have an Action on a Controller that i'm using for LinkedIn auth. The first time the user hits the controller it redirects to the LinkedIn site for authentication, once the user authenticates linked in redirects back to the same controller with the auth code in the url as a param.
http://beta.consynki.com/authentication/network/linkedin?code=DQTdGfxIlbsU...
Controller
class AuthenticationController extends WebController {
public function actionNetwork($network){
$access_token = Yii::$app->request->get('code');
$network_connection = NetworkFactory::build($network);
$client = $network_connection->getClient();
if($access_token && !is_null($access_token)){
$headers = Yii::$app->response->headers;
$headers->set('Pragma', 'no-cache');
$headers->add('X-Access-Token', $access_token);
return $this->render('success');
}
return $this->redirect($client->getLoginUrl(),302)->send();
}
}
EDIT 1 - WebController
/**
* Class WebController
*
* Default controller for public web pages. This class pulls meta tags from a seporately stored file, and makes
* them available to the view.
*
* #package www\components
*/
class WebController extends Controller {
public $meta = [];
public function beforeAction($event) {
$controller = $this->id;
$action = $this->action->id;
$meta_file = Yii::getAlias('#www') . '/meta/' . $controller . '/' . $action . '.php';
if (file_exists($meta_file)) {
$this->meta = include($meta_file);
$this->setMetaTags($this->meta);
}
return parent::beforeAction($event);
}
/**
* Set the meta tags for a page
*
* #param string $type
* #param $tag
* #param $value
*/
public function registerMetaTag($type = 'name', $tag, $value) {
if (!is_null($value)) {
$this->view->registerMetaTag([
$type => $tag,
'content' => $value
]);
}
}
public function behaviors() {
return [
/**
* The particular campaign used.
*
* Example social_share, stay_connected_add
*/
'utm_campaign' => [
'class' => 'common\behavior\TrackingBehavior',
'queryParam' => 'utm_campaign',
'sessionParam' => 'utm_campaign',
],
/*
* The source of the referral, could be an add network, facebook, email or just a link on the Consynki site.
*
* example: google, facebook, citysearch, welcome_email
*/
'utm_source' => [
'class' => 'common\behavior\TrackingBehavior',
'queryParam' => 'utm_source',
'sessionParam' => 'utm_source',
],
];
}
protected function setMetaTags($meta) {
foreach ($meta AS $key => $value) {
if (is_array($value)) {
$this->view->registerMetaTag($value, $key);
}
}
}
}
Problem
When I try to get the code from the GET param Yii::$app->request->get('code'); I get a NULL value. On further inspection of the $_GET array var_dump($app->request->get() or var_dump($_GET); I see the key for the code variable has a $ "?code" in front of it. This is very odd.
array(3) { ["network"]=> string(8) "linkedin" ["?code"]=> string(115) "DQTdGfxIlbsU..." ["state"]=> string(32) "12121..." }
Research Notes
It looks like Yii2 modify's the $_GET value as it passes the url routing. Not sure if this is the issue. Have updated to the latest version of yii and it didn't fix the problem.
Question
Why would this happen? How can I fix it so that I can get the code value?
Set rules there like this:
'authentication/network/<network:\w+>/<code:\w+>' => 'authentication/network',
'authentication/network/<network:\w+>' => 'authentication/network',
Now in action set parameters like:
public function actionNetwork($network, $code = null)
Your previous $access_token is now $code.

How tu use recaptcha google with phalcon framework

I'm still trying to add a recaptcha to my website, I want try the recaptcha from Google but I can't use it properly. Checked or not, my email is still sent.
I tried to understand the code of How to validate Google reCaptcha v2 using phalcon/volt forms?.
But i don't understand where are my problems and more over how can you create an element like
$recaptcha = new Check('recaptcha');
My controller implementation :
<?php
/**
* ContactController
*
* Allows to contact the staff using a contact form
*/
class ContactController extends ControllerBase
{
public function initialize()
{
$this->tag->setTitle('Contact');
parent::initialize();
}
public function indexAction()
{
$this->view->form = new ContactForm;
}
/**
* Saves the contact information in the database
*/
public function sendAction()
{
if ($this->request->isPost() != true) {
return $this->forward('contact/index');
}
$form = new ContactForm;
$contact = new Contact();
// Validate the form
$data = $this->request->getPost();
if (!$form->isValid($data, $contact)) {
foreach ($form->getMessages() as $message) {
$this->flash->error($message);
}
return $this->forward('contact/index');
}
if ($contact->save() == false) {
foreach ($contact->getMessages() as $message) {
$this->flash->error($message);
}
return $this->forward('contact/index');
}
$this->flash->success('Merci, nous vous contacterons très rapidement');
return $this->forward('index/index');
}
}
In my view i added :
<div class="g-recaptcha" data-sitekey="mypublickey0123456789"></div>
{{ form.messages('recaptcha') }}
But my problem is after : i create a new validator for the recaptcha like in How to validate Google reCaptcha v2 using phalcon/volt forms? :
use \Phalcon\Validation\Validator;
use \Phalcon\Validation\ValidatorInterface;
use \Phalcon\Validation\Message;
class RecaptchaValidator extends Validator implements ValidatorInterface
{
public function validate(\Phalcon\Validation $validation, $attribute)
{
if (!$this->isValid($validation)) {
$message = $this->getOption('message');
if ($message) {
$validation->appendMessage(new Message($message, $attribute, 'Recaptcha'));
}
return false;
}
return true;
}
public function isValid($validation)
{
try {
$value = $validation->getValue('g-recaptcha-response');
$ip = $validation->request->getClientAddress();
$url = $config->'https://www.google.com/recaptcha/api/siteverify'
$data = ['secret' => $config->mysecretkey123456789
'response' => $value,
'remoteip' => $ip,
];
// Prepare POST request
$options = [
'http' => [
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => http_build_query($data),
],
];
// Make POST request and evaluate the response
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
return json_decode($result)->success;
}
catch (Exception $e) {
return null;
}
}
}
So i don't know if tjis code is correct anyway, i have a problem too after that : how to create an object "recaptcha" in my form add
$recaptcha = new ?????('recaptcha');
$recaptcha->addValidator(new RecaptchaValidator([
'message' => 'Please confirm that you are human'
]));
$this->add($recaptcha);
PS: I apologize because i'm a noob here and my mother tongue is not english, so if you don't understand me or want give me some advices to create a proper question, don't hesitate ^^
I've made a custom form element for recaptcha. Used it for many projects so far.
The form element class:
class Recaptcha extends \Phalcon\Forms\Element
{
public function render($attributes = null)
{
$html = '<script src="https://www.google.com/recaptcha/api.js?hl=en"></script>';
$html.= '<div class="g-recaptcha" data-sitekey="YOUR_PUBLIC_KEY"></div>';
return $html;
}
}
The recaptcha validator class:
use Phalcon\Validation\Validator;
use Phalcon\Validation\ValidatorInterface;
use Phalcon\Validation\Message;
class RecaptchaValidator extends Validator implements ValidatorInterface
{
public function validate(\Phalcon\Validation $validation, $attribute)
{
$value = $validation->getValue('g-recaptcha-response');
$ip = $validation->request->getClientAddress();
if (!$this->verify($value, $ip)) {
$validation->appendMessage(new Message($this->getOption('message'), $attribute, 'Recaptcha'));
return false;
}
return true;
}
protected function verify($value, $ip)
{
$params = [
'secret' => 'YOUR_PRIVATE_KEY',
'response' => $value,
'remoteip' => $ip
];
$response = json_decode(file_get_contents('https://www.google.com/recaptcha/api/siteverify?' . http_build_query($params)));
return (bool)$response->success;
}
}
Using in your form class:
$recaptcha = new Recaptcha($name);
$recaptcha->addValidator(new RecaptchaValidator([
'message' => 'YOUR_RECAPTCHA_ERROR_MESSAGE'
]));
Note 1: You were almost there, you just missed to create custom form element (the first and last code piece from my example);
Note 2: Also there is a library in Github: https://github.com/fizzka/phalcon-recaptcha I have not used it, but few peeps at phalcon forum recommended it.

Elasticsearch in laravel 5.1

I want to integrate elasticsearch in my laravel project.
I have installed using following line :
Run command on terminal :
composer require shift31/laravel-elasticsearch:~1.0
Then i have created elasticsearch.php in app/config/ and added following code.
<?php
use Monolog\Logger;
return array(
'hosts' => array(
'your.elasticsearch.server:9200' // what should be my host ?
),
'logPath' => 'path/to/your/elasticsearch/log',
'logLevel' => Logger::INFO
);
My first question : What should i write in place of host name
Right now my project is running on local server with localhost:8000.
I have added Shift31\LaravelElasticsearch\ElasticsearchServiceProvider in app/config/app.php for enable the 'Es' facade.
Above all things done. Now in which file i should add the code of elasticsearch to add, update, delete and search the records.
I have product table I need to add product records in elasticsearch, when update product, records should be update.
I have no idea of the further process. Please guide me I have searched on google but no any example help me.
Create the following helper classes in their respective paths:
App\Traits\ElasticSearchEventTrait.php
<?php
Namespace App\Traits;
trait ElasticSearchEventTrait {
public $esRemoveDefault = array('created_at','updated_at','deleted_at');
public static function boot()
{
parent::boot();
static::bootElasticSearchEvent();
}
public static function bootElasticSearchEvent()
{
static::created(function ($model) {
if(isset($model->esEnabled) && $model->esEnabled === true)
{
$model->esCreate();
}
});
static::updated(function ($model) {
if(isset($model->esEnabled) && $model->esEnabled === true)
{
$model->esUpdate();
}
});
static::deleted(function ($model) {
if(isset($model->esEnabled) && $model->esEnabled === true)
{
$model->esUpdate();
}
});
}
private function esCreate()
{
//esContext is false for polymorphic relations with no elasticsearch indexing
if(isset($this->esMain) && $this->esMain === true && $this->esContext !== false)
{
\Queue::push('ElasticSearchHelper#indexTask',array('id'=>$this->esGetId(),'class'=>get_class($this),'context'=>$this->esGetContext(),'info-context'=>$this->esGetInfoContext(),'excludes'=>$this->esGetRemove()));
}
else
{
$this->esUpdate();
}
}
private function esUpdate()
{
//esContext is false for polymorphic relations with no elasticsearch indexing
if($this->esContext !== false)
{
\Queue::push('ElasticSearchHelper#updateTask',array('id'=>$this->esGetId(),'class'=>get_class($this),'context'=>$this->esGetContext(),'info-context'=>$this->esGetInfoContext(),'excludes'=>$this->esGetRemove()));
}
}
/*
* Get Id of Model
*/
public function esGetId()
{
if(isset($this->esId))
{
return $this->esId;
}
else
{
return $this->id;
}
}
public function esGetInfoContext()
{
if(isset($this->esInfoContext))
{
return $this->esInfoContext;
}
else
{
throw new \RuntimeException("esInfoContext attribute or esGetInfoContext() is not set in class '".get_class($this)."'");
}
}
/*
* Name of main context of model
*/
public function esGetContext()
{
if(isset($this->esContext))
{
return $this->esContext;
}
else
{
throw new \RuntimeException("esContext attribute or esGetContext() method must be set in class '".get_class($this)."'");
}
}
/*
* All attributes that needs to be removed from model
*/
public function esGetRemove()
{
if(isset($this->esRemove))
{
return array_unique(array_merge($this->esRemoveDefault,$this->esRemove));
}
else
{
return $this->esRemoveDefault;
}
}
/*
* Extends Illuminate Collection to provide additional array functions
*/
public function newCollection(array $models = Array())
{
return new Core\Collection($models);
}
/**
* Return a timestamp as DateTime object.
*
* #param mixed $value
* #return \Carbon\Carbon
*/
public function asEsDateTime($value)
{
// If this value is an integer, we will assume it is a UNIX timestamp's value
// and format a Carbon object from this timestamp. This allows flexibility
// when defining your date fields as they might be UNIX timestamps here.
if (is_numeric($value))
{
return \Carbon::createFromTimestamp($value);
}
// If the value is in simply year, month, day format, we will instantiate the
// Carbon instances from that format. Again, this provides for simple date
// fields on the database, while still supporting Carbonized conversion.
elseif (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $value))
{
return \Carbon::createFromFormat('Y-m-d', $value)->startOfDay();
}
// Finally, we will just assume this date is in the format used by default on
// the database connection and use that format to create the Carbon object
// that is returned back out to the developers after we convert it here.
elseif ( ! $value instanceof DateTime)
{
$format = $this->getEsDateFormat();
return \Carbon::createFromFormat($format, $value);
}
return \Carbon::instance($value);
}
/**
* Get the format for database stored dates.
*
* #return string
*/
private function getEsDateFormat()
{
return $this->getConnection()->getQueryGrammar()->getDateFormat();
}
/*
* Converts model to a suitable format for ElasticSearch
*/
public function getEsSaveFormat()
{
$obj = clone $this;
//Go through ES Accessors
\ElasticSearchHelper::esAccessor($obj);
$dates = $this->getDates();
//Convert to array, then change Date to appropriate Elasticsearch format.
//Why? Because eloquent's date accessors is playing me.
$dataArray = $obj->attributesToArray();
//Remove all Excludes
foreach($this->esGetRemove() as $ex)
{
if(array_key_exists($ex,$dataArray))
{
unset($dataArray[$ex]);
}
}
if(!empty($dates))
{
foreach($dates as $d)
{
if(isset($dataArray[$d]) && $dataArray[$d] !== "" )
{
//Trigger Eloquent Getter which will provide a Carbon instance
$dataArray[$d] = $this->{$d}->toIso8601String();
}
}
}
return $dataArray;
}
}
App\Services\ElasticServiceHelper.php
<?php
/**
* Description of ElasticSearchHelper: Helps with Indexing/Updating with Elastic Search Server (https://www.elastic.co)
*
* #author kpudaruth
*/
Namespace App\Services;
class ElasticSearchHelper {
/*
* Laravel Queue - Index Task
* #param array $job
* #param array $data
*/
public function indexTask($job,$data)
{
if(\Config::get('website.elasticsearch') === true)
{
if(isset($data['context']))
{
$this->indexEs($data);
}
else
{
\Log::error('ElasticSearchHelper: No context set for the following dataset: '.json_encode($data));
}
}
$job->delete();
}
/*
* Laravel Queue - Update Task
* #param array $job
* #param array $data
*/
public function updateTask($job,$data)
{
if(\Config::get('website.elasticsearch') === true)
{
if(isset($data['context']))
{
$this->updateEs($data);
}
else
{
\Log::error('ElasticSearchHelper: No context set for the following dataset: '.json_encode($data));
}
}
$job->delete();
}
/*
* Index Elastic Search Document
* #param array $data
*/
public function indexEs($data)
{
$params = array();
$params['index'] = \App::environment();
$params['type'] = $data['context'];
$model = new $data['class'];
$form = $model::find($data['id']);
if($form)
{
$params['id'] = $form->id;
if($form->timestamps)
{
$params['timestamp'] = $form->updated_at->toIso8601String();
}
$params['body'][$data['context']] = $this->saveFormat($form);
\Es::index($params);
}
}
/*
* Update Elastic Search
* #param array $data
*/
public function updateEs($data)
{
$params = array();
$params['index'] = \App::environment();
$params['type'] = $data['context'];
$model = new $data['class'];
$form = $model::withTrashed()->find($data['id']);
if(count($form))
{
/*
* Main form is being updated
*/
if($data['info-context'] === $data['context'])
{
$params['id'] = $data['id'];
$params['body']['doc'][$data['info-context']] = $this->saveFormat($form);
}
else
{
//Form is child, we get parent
$parent = $form->esGetParent();
if(count($parent))
{
//Id is always that of parent
$params['id'] = $parent->id;
//fetch all children, given that we cannot save per children basis
$children = $parent->{$data['info-context']}()->get();
if(count($children))
{
//Get data in a format that can be saved by Elastic Search
$params['body']['doc'][$data['info-context']] = $this->saveFormat($children);
}
else
{
//Empty it is
$params['body']['doc'][$data['info-context']] = array();
}
}
else
{
\Log::error("Parent not found for {$data['context']} - {$data['class']}, Id: {$data['id']}");
return false;
}
}
//Check if Parent Exists
try
{
$result = \Es::get([
'id' => $params['id'],
'index' => $params['index'],
'type' => $data['context']
]);
} catch (\Exception $ex) {
if($ex instanceof \Elasticsearch\Common\Exceptions\Missing404Exception || $ex instanceof \Guzzle\Http\Exception\ClientErrorResponseException)
{
//if not, we set it
if (isset($parent) && $parent)
{
$this->indexEs([
'context' => $data['context'],
'class' => get_class($parent),
'id' => $parent->id,
]);
}
else
{
\Log::error('Unexpected error in updating elasticsearch records, parent not set with message: '.$ex->getMessage());
return false;
}
}
else
{
\Log::error('Unexpected error in updating elasticsearch records: '.$ex->getMessage());
return false;
}
}
\Es::update($params);
}
}
/*
* Iterate through all Es accessors of the model.
* #param \Illuminate\Database\Eloquent\Model $object
*/
public function esAccessor(&$object)
{
if(is_object($object))
{
$attributes = $object->getAttributes();
foreach($attributes as $name => $value)
{
$esMutator = 'get' . studly_case($name) . 'EsAttribute';
if (method_exists($object, $esMutator)) {
$object->{$name} = $object->$esMutator($object->{$name});
}
}
}
else
{
throw New \RuntimeException("Expected type object");
}
}
/*
* Iterates over a collection applying the getEsSaveFormat function
* #param mixed $object
*
* #return array
*/
public function saveFormat($object)
{
if($object instanceof \Illuminate\Database\Eloquent\Model)
{
return $object->getEsSaveFormat();
}
else
{
return array_map(function($value)
{
return $value->getEsSaveFormat();
}, $object->all());
}
}
}
A couple of gotchas from the above helper classes:
The default ElasticSearch index is set to the name of the App's Environment
The ..task() functions are meant for the old laravel 4.2 queue format. I've yet to port those to laravel 5.x. Same goes for the Queue::push commands.
Example
ElasticSearch Mapping:
[
'automobile' => [
"dynamic" => "strict",
'properties' => [
'automobile' => [
'properties' => [
'id' => [
'type' => 'long',
'index' => 'not_analyzed'
],
'manufacturer_name' => [
'type' => 'string',
],
'manufactured_on' => [
'type' => 'date'
]
]
],
'car' => [
'properties' => [
'id' => [
'type' => 'long',
'index' => 'not_analyzed'
],
'name' => [
'type' => 'string',
],
'model_id' => [
'type' => 'string'
]
]
],
"car-model" => [
'properties' => [
'id' => [
'type' => 'long',
'index' => 'not_analyzed'
],
'description' => [
'type' => 'string',
],
'name' => [
'type' => 'string'
]
]
]
]
]
]
Top level document is called 'automobile'. Underneath it, you have 'automobile', 'car' & 'car-model'. Consider 'car' & 'car-model' as relations to the automobile. They are known as sub documents on elasticsearch. (See: https://www.elastic.co/guide/en/elasticsearch/guide/current/document.html)
Model: App\Models\Car.php
namespace App\Models;
class Car extends \Eloquent {
use \Illuminate\Database\Eloquent\SoftDeletingTrait;
use \App\Traits\ElasticSearchEventTrait;
protected $table = 'car';
protected $fillable = [
'name',
'serie',
'model_id',
'automobile_id'
];
protected $dates = [
'deleted_at'
];
/* Elastic Search */
//Indexing Enabled
public $esEnabled = true;
//Context for Indexing - Top Level name in the mapping
public $esContext = "automobile";
//Info Context - Secondary level name in the mapping.
public $esInfoContext = "car";
//The following fields will not be saved in elasticsearch.
public $esRemove = ['automobile_id'];
//Fetches parent relation of car, so that we can retrieve its id for saving in the appropriate elasticsearch record
public function esGetParent()
{
return $this->automobile;
}
/*
* Event Observers
*/
public static function boot() {
parent:: boot();
//Attach events to model on start
static::bootElasticSearchEvent();
}
/*
* ElasticSearch Accessor
*
* Sometimes you might wish to format the data before storing it in elasticsearch,
* The accessor name is in the format of: get + attribute's name camel case + EsAttribute
* The $val parameter will always be the value of the attribute that is being accessed.
*
* #param mixed $val
*/
/*
* Elasticsearch Accessor: Model Id
*
* Get the model name and save it
*
* #param int $model_id
* #return string
*/
public function getModelIdEsAttribute($model_id) {
//Fetch model from table
$model = \App\Models\CarModel::find($model_id);
if($model) {
//Return name of model if found
return $model->name;
} else {
return '';
}
}
/*
* Automobile Relationship: Belongs To
*/
public function automobile()
{
return $this->belongsTo('\App\Models\Automobile','automobile_id');
}
}
Example of Search Query:
/**
* Get search results
*
* #param string $search (Search string)
*
*/
public function getAll($search)
{
$params = array();
$params['index'] = App::environment();
//Declare your mapping names in the array which you wish to search on.
$params['type'] = array('automobile');
/*
* Build Query String
*/
//Exact match is favored instead of fuzzy ones
$params['body']['query']['bool']['should'][0]['match']['name']['query'] = $search;
$params['body']['query']['bool']['should'][0]['match']['name']['operator'] = "and";
$params['body']['query']['bool']['should'][0]['match']['name']['boost'] = 2;
$params['body']['query']['bool']['should'][1]['fuzzy_like_this']['like_text'] = $search;
$params['body']['query']['bool']['should'][1]['fuzzy_like_this']['fuzziness'] = 0.5;
$params['body']['query']['bool']['should'][1]['fuzzy_like_this']['prefix_length'] = 2;
$params['body']['query']['bool']['minimum_should_match'] = 1;
//Highlight matches
$params['body']['highlight']['fields']['*'] = new \stdClass();
$params['body']['highlight']['pre_tags'] = array('<b>');
$params['body']['highlight']['post_tags'] = array('</b>');
//Exclude laravel timestamps
$params['body']['_source']['exclude'] = array( "*.created_at","*.updated_at","*.deleted_at");
/*
* Poll search server until we have some results
*/
$from_offset = 0;
$result = array();
//Loop through all the search results
do
{
try
{
$params['body']['from'] = $from_offset;
$params['body']['size'] = 5;
$queryResponse = \Es::search($params);
//Custom function to process the result
//Since we will receive a bunch of arrays, we need to reformat the data and display it properly.
$result = $this->processSearchResult($queryResponse);
$from_offset+= 5;
}
catch (\Exception $e)
{
\Log::error($e->getMessage());
return Response::make("An error occured with the search server.",500);
}
}
while (count($result) === 0 && $queryResponse['hits']['total'] > 0);
echo json_encode($result);
}
/*
* Format search results as necessary
* #param array $queryResponse
*/
private function processSearchResult(array $queryResponse)
{
$result = array();
//Check if we have results in the array
if($queryResponse['hits']['total'] > 0 && $queryResponse['timed_out'] === false)
{
//Loop through each result
foreach($queryResponse['hits']['hits'] as $line)
{
//Elasticsearch will highlight the relevant sections in your query in an array. The below creates a readable format with · as delimiter.
$highlight = "";
if(isset($line['highlight']))
{
foreach($line['highlight'] as $k=>$v)
{
foreach($v as $val)
{
$highlight[] = str_replace("_"," ",implode(" - ",explode(".",$k)))." : ".$val;
}
}
$highlight = implode(" · ",$highlight);
}
//Check the mapping type
switch($line['_type'])
{
case "automobile":
$result[] = array('icon'=>'fa-automobile',
'title'=> 'Automobile',
'id' => $line['_id'],
//name to be displayed on my search result page
'value'=>$line['_source'][$line['_type']]['name']." (Code: ".$line['_id'].")",
//Using a helper to generate the url. Build your own class.
'url'=>\App\Helpers\URLGenerator::generate($line['_type'],$line['_id']),
//And the highlights as formatted above.
'highlight'=>$highlight);
break;
}
}
}
return $result;
}

Fields getting NULL after editing

I have developed Zend 2 application. There is form to edit existing data. Some fields in table is not included in the form. Thus, when editing those records, fields not in form are saved as NULL. How to fix it ?
Model -
namespace Employee\Model;
class Employee
{
public $id;
public $active;
public $type;
public $mailing_address;
public $permanent_address;
...
public function exchangeArray($data)
{
$this->id = (isset($data['id'])) ? $data['id'] : 0;
$this->active = (isset($data['active'])) ? $data['active'] : 0;
$this->type = (isset($data['type'])) ? $data['type'] : null;
$this->mailing_address = (isset($data['mailing_address'])) ? $data['mailing_address'] : null;
$this->permanent_address = (isset($data['permanent_address'])) ? $data['permanent_address'] : null;
...
Table -
public function saveEmployee(Employee $employee) {
$data = array(
'active' => $employee->active,
'type' => $employee->type,
'mailing_address' => $employee->mailing_address,
'permanent_address' => $employee->permanent_address,
...
$id = (int) $employee->id;
if ($id == 0) {
$inserted = $this->tableGateway->insert($data);
$inserted_id = $this->tableGateway->lastInsertValue;
} else {
if ($this->getEmployee($id)) {
$this->tableGateway->update($data, array('id' => $id));
$inserted_id = $id;
} else {
throw new \Exception('Employee does not exist');
}
}
return $inserted_id;
//\Zend\Debug\Debug::dump($inserted_ids);
}
Controller -
$employeeForm = new EmployeeForm();
$employeeForm->bind($employee);
$request = $this->getRequest();
if ($request->isPost()) {
$employeeForm->setData($request->getPost());
if ($employeeForm->isValid()) {
$this->getEmployeeTable()->saveEmployee($employee);
}
}
Assume type dosn't have form filed defined. So, it shouldn't get NULL when save.
How to fix it ?
try handling it with mysql. use the [default] function of each field wisely
CREATE TABLE `table` (
`type` tinyint(3) unsigned NOT NULL default '0',
.......................
If you are editing an existing record then you would need to first load all the data for that entity and then update the fields that have changed. In ZF2 this is achieved via a form hydrator; as you bind the 'populated' object to the form.
Therefore your controller code would need to change.
EmpolyeeController.php
// Fetch the form from the service manager
// allowing it to be created via factory and have our
// hydrator and entity class injected
$form = $this->serviceLocator()->get('MyModule\Form\EmployeeForm');
$request = $this->getRequest();
$id = $this->params('id'); // Employee ID as route param
// Load the employee data from the database
// (this will vary dependning your own strategy, however
// a service layer is assumed)
$employee = $this->employeeService->findById($id);
// Bind the **hydrated** entity to the form
$form->bind($employee);
if ($request->isPost()) {
// set the modified post data
$form->setData($request->getPost());
if ($form->isValid()) {
// Retrive the validated and updated entity
$employee = $form->getData();
}
}
You will also need to register a form factory to inject the hydrator (an other dependancies).
Module.php
public function getFormElementConifg()
{
return array(
'factories' => array(
'MyModule\Form\EmployeeForm' => function($formElementManager) {
$serviceManager = $formElementManager->getServiceLocator();
$hydrator = $serviceManager->get('MyModule\Stdlib\Hydrator\EmployeeHydrator');
$form = new Form\EmployeeForm();
$form->setHydrator($hydrator);
$form->bind(new Entity\Employee());
return $form;
}
),
)
}

Categories