Check existence in Doctrine for persisted and not persisted entities - symfony

My question could be a duplicate of this one, but I can't find any satisfying answer so I will try to make this one more precise.
I am building an import service from an other API. And I don't want any duplicate in my new database.
So here an example of my current implementation:
The Controller:
public function mainAction ()
{
$em = $this->getDoctrine()->getManager();
$persons_data = [
[
'first_name' => 'John',
'last_name' => 'Doe'
],
[
'first_name' => 'John',
'last_name' => 'Doe'
]
];
$array = [];
foreach($persons_data as $person_data)
{
$person = $this->get('my_service')->findOrCreatePerson($person_data);
$array[] = $person;
}
$em->flush();
return new Response();
}
A service function:
public function findOrCreatePerson ($data)
{
$em = $this->em;
$person = $em->getRepository('AppBundle:Person')->findOneBy([
'first_name' => $data['first_name'],
'last_name' => $data['last_name']
]);
if(is_null($person)) {
$person = new Person();
$person->setFirstName($data['first_name']);
$person->setLastName($data['last_name']);
$em->persist($person);
}
return $person
}
I tried to make it as simple as possible.
As you can see, I would like to make only one DB transaction to get some performance improvements.
Problem is, if I don't flush at the end of the findOrCreatePerson() method, the query to the Person repository won't find the first object and will create duplicates in the database.
My question is simple: How should I implement such a thing?

This is a job for memoize!
// Cache
private $persons = [];
public function findOrCreatePerson ($data)
{
// Need unique identifier for persons
$personKey = $data['first_name'] . $data['last_name'];
// Already processed ?
if (isset($this->persons[$personKey])) {
return $this->persons[$personKey];
}
$em = $this->em;
$person = $em->getRepository('AppBundle:Person')->findOneBy([
'first_name' => $data['first_name'],
'last_name' => $data['last_name']
]);
if(is_null($person)) {
$person = new Person();
$person->setFirstName($data['first_name']);
$person->setLastName($data['last_name']);
$em->persist($person);
}
// Cache
$this->persons[$personKey] = $person;
return $person
}

Cerad's answer (memoization) is a good one, but I'd encourage you to reconsider something.
As you can see, I would like to make only one DB transaction to get some performance improvements.
And there are a few things wrong with that sentence.
The main one is that you're conflating flush() with a single, atomic transaction. You can manually manage transaction boundaries, and it's often very advantageous to do so.
The second thing is that when you're talking about bulk imports, you'll quickly learn that the first performance issue you hit is not the database at all. It's the the EntityManager itself. As the EM's internal identity map gets swollen, computing changes to persist to the DB gets very, very slow.
I'd consider rewriting your core loop as follows, and see if it's fast enough. Only then consider memoization if necessary.
$em->beginTransaction();
foreach($persons_data as $person_data)
{
$person = $this->get('my_service')->findOrCreatePerson($person_data);
$em->flush();
$em->clear(); // don't keep previously inserted entities in the EM.
}
$em->commit();

Related

Async Await with four nested loops

I am currently trying to return a JSON object array that requires me to do one asynchronous function and then four nested asynchronous map functions in order to populate an array of entities. Basically, each user has an array of orders, each order has an array of items, each item has an array of options and each option has an array of values. I am using loopback4 framework and therefore cannot do res.send once all things have been populated. The function seems to return on the first await, but any await after that, it does not wait on it and instead runs to the end of the function. I have tried using Promises and .thens(), but cannot seem to figure out how to populate each entity fully nested, and then return the array of populated entities. I keep getting an empty array. Below is only one nest of maps, but I cannot get it to even populate up to the first nest and return this, so I decided not to go any further. This is the code:
async getUserOrders2(#param.path.number('id') id: number): Promise<any> {
if ( !this.user) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
}
else if (this.user.id != id) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
}
else {
let restaurantId = this.user.restaurantId
let orderFrameArray = new Array<OrderFrame>()
return this.restaurantRepository.orders(restaurantId as string).find()
.then(async orders => {
orders.map(async (val, key)=> {
let orderFrame = new OrderFrame(val)
orderFrame.itemArray = await this.orderRepository.orderItems(val.id).find()
orderFrameArray.push(orderFrame)
})
orderFrameArray = await Promise.all(orderFrameArray)
return orderFrameArray
})
}
}
The function is returning before the orderFrameArray has been populated. I need four nested map loops and this first one is not working, so I am not sure how to do the rest. Any help would be extremely appreciated.
Based on #Tomalaks solution I tried the following, but its still only returning the top level array and nothing is nested:
async getUserOrders2(#param.path.number('id') id: number): Promise<any> {
if ( !this.user) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
}
else if (this.user.id != id) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
}
else {
let restaurantId = this.user.restaurantId
let orderFrameArray = new Array<OrderFrame>()
return this.restaurantRepository.orders(restaurantId as string).find()
.then(orders => {Promise.all(orders.map(
order => {
let orderFrame = new OrderFrame(order)
orderFrame.itemArray = new Array<Item>()
this.orderRepository.orderItems(order.id).find()
.then(orderItems => Promise.all(orderItems.map(
orderItem => {
let itemFrame = new Item(orderItem)
itemFrame.options = new Array<Option>()
this.orderItemRepository.orderItemOptions(orderItem.id).find()
.then(orderItemOptions => Promise.all(orderItemOptions.map(
orderItemOption => {
let optionFrame = new Option(orderItemOption)
optionFrame.values = new Array<Value>()
this.orderItemOptionRepository.orderItemOptionValues(orderItemOption.id).find()
.then(orderItemOptionValues => Promise.all(orderItemOptionValues.map(
orderItemOptionValue => {
let valueFrame = new Value(orderItemOptionValue)
optionFrame.values.push(valueFrame)})))
itemFrame.options.push(optionFrame)})))
orderFrame.itemArray.push(itemFrame)})))
orderFrameArray.push(orderFrame)}))
return orderFrameArray})
}
}
I apologize for the formatting I wasn't sure how best to format it. Is there something else I'm doing wrong?
Thanks to everyone for their response. The answer that was posted by #Tomalak was correct. I just had to surround the entire function in brackets, and put a .then to return the populated entity I had made
You only need to use async when you are using await in the same function. If there's await in a nested function, the parent function does not need async.
However, in your case, there is no function that should be made async in the first place.
There is no benefit in awaiting any results in your function, because no code inside depends on any intermediary result. Just return the promises as you get them.
There's no need for intermediary result variables like orderFrameArray, you're making things harder than they are with your approach of awaiting individual orders and pushing them to a top-level variable.
Using await in a loop like you do inside your .map() call is bad for performance. You are basically serializing database access this way – the next query will only be sent after the current one has returned. This kind of daisy-chaining nullifies the database's ability to process multiple concurrent requests.
getUserOrders2 is not Promise<any>, it's Promise<Array<OrderFrame>>.
throw terminates the function anyway, you can do multiple checks for error conditions without using else if. This reduces nesting.
So a fully asynchronous function would look like this:
getUserOrders2(#param.path.number('id') id: number): Promise<Array<OrderFrame>> {
if (!this.user) throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
if (this.user.id != id) throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
return this.restaurantRepository
.orders(this.user.restaurantId).find().then(
orders => Promise.all(orders.map(
order => this.orderRepository.orderItems(order.id).find().then(
order => new OrderFrame(order)
)
))
);
}
The async/await equivalent of this function would be more complex.
You then would await the result in the calling code, as you would have to do anyway:
async test() {
const orders = await foo.getUserOrders2(someUserId);
// ...
}
// or
test() {
foo.getUserOrders2(someUserId).then(orders => {
// ...
});
}

Stripe test payment and saving order simultaneously not working

I am working with stripe test payment and want to store my order in db also but facing an error i.e.
Relationship method must return an object of type Illuminate\Database\Eloquent\Relations\Relation
My controller is Checkout function
public function postCheckOut(Request $request){
if(!Session::has('cart')){
return redirect()->route('shop.cart');
}
$oldCart = Session::get('cart');
$cart = new Cart($oldCart);
/* Stripe Test Api key */
Stripe::setApiKey('-------');
try{
$charge = \Stripe\Charge::create(array(
"amount" => $cart->totalPrice * 100,
"currency" => "usd",
// "source" => "$request->input('stripeToken')", // obtained with Stripe.js
'card' => array(
'number' => $request->get("cnumber"),
'exp_month' => $request->get("exp-month"),
'exp_year' => $request->get("exp-year"),
'cvc' => $request->get("cvc"),
),
"description" => "product purchased"
));
/* Saving order data after purchasing done */
$order = new Order();
$order->user_id = Auth::id();
$order->cart = serialize($cart);
$order->address = $request->input('address');
$order->name = $request->input('name');
$order->payment_id = $charge->id;
// dd($order);exit;
/* Saving order data by orm relation calling login user then its order function and then save orders */
Auth::user()->orders->save($order);
} catch (\Exception $e){
return redirect()->route('checkout')->with('error', $e->getMessage());
}
Session::forget('cart');
return redirect()->route('product.index')->with('success', 'Product purchased');
}
please guide what i am doing wrong. I check almost what i can.
Because you are already getting value for foreign key Try directly storing it like this
$order->save();
else you can also try this
Auth::user()->orders()->save($order);
instead of
Auth::user()->orders->save($order);
Refer to docs

Batch requests on Symfony

I am trying to reproduce the behaviour of the facebook batch requests function on their graph api.
So I think that the easiest solution is to make several requests on a controller to my application like:
public function batchAction (Request $request)
{
$requests = $request->all();
$responses = [];
foreach ($requests as $req) {
$response = $this->get('some_http_client')
->request($req['method'],$req['relative_url'],$req['options']);
$responses[] = [
'method' => $req['method'],
'url' => $req['url'],
'code' => $response->getCode(),
'headers' => $response->getHeaders(),
'body' => $response->getContent()
]
}
return new JsonResponse($responses)
}
So with this solution, I think that my functional tests would be green.
However, I fill like initializing the service container X times might make the application much slower. Because for each request, every bundle is built, the service container is rebuilt each time etc...
Do you see any other solution for my problem?
In other words, do I need to make complete new HTTP requests to my server to get responses from other controllers in my application?
Thank you in advance for your advices!
Internally Symfony handle a Request with the http_kernel component. So you can simulate a Request for every batch action you want to execute and then pass it to the http_kernel component and then elaborate the result.
Consider this Example controller:
/**
* #Route("/batchAction", name="batchAction")
*/
public function batchAction()
{
// Simulate a batch request of existing route
$requests = [
[
'method' => 'GET',
'relative_url' => '/b',
'options' => 'a=b&cd',
],
[
'method' => 'GET',
'relative_url' => '/c',
'options' => 'a=b&cd',
],
];
$kernel = $this->get('http_kernel');
$responses = [];
foreach($requests as $aRequest){
// Construct a query params. Is only an example i don't know your input
$options=[];
parse_str($aRequest['options'], $options);
// Construct a new request object for each batch request
$req = Request::create(
$aRequest['relative_url'],
$aRequest['method'],
$options
);
// process the request
// TODO handle exception
$response = $kernel->handle($req);
$responses[] = [
'method' => $aRequest['method'],
'url' => $aRequest['relative_url'],
'code' => $response->getStatusCode(),
'headers' => $response->headers,
'body' => $response->getContent()
];
}
return new JsonResponse($responses);
}
With the following controller method:
/**
* #Route("/a", name="route_a_")
*/
public function aAction(Request $request)
{
return new Response('A');
}
/**
* #Route("/b", name="route_b_")
*/
public function bAction(Request $request)
{
return new Response('B');
}
/**
* #Route("/c", name="route_c_")
*/
public function cAction(Request $request)
{
return new Response('C');
}
The output of the request will be:
[
{"method":"GET","url":"\/b","code":200,"headers":{},"body":"B"},
{"method":"GET","url":"\/c","code":200,"headers":{},"body":"C"}
]
PS: I hope that I have correctly understand what you need.
There are ways to optimise test-speed, both with PHPunit configuration (for example, xdebug config, or running the tests with the phpdbg SAPI instead of including the Xdebug module into the usual PHP instance).
Because the code will always be running the AppKernel class, you can also put some optimisations in there for specific environments - including initiali[zs]ing the container less often during a test.
I'm using one such example by Kris Wallsmith. Here is his sample code.
class AppKernel extends Kernel
{
// ... registerBundles() etc
// In dev & test, you can also set the cache/log directories
// with getCacheDir() & getLogDir() to a ramdrive (/tmpfs).
// particularly useful when running in VirtualBox
protected function initializeContainer()
{
static $first = true;
if ('test' !== $this->getEnvironment()) {
parent::initializeContainer();
return;
}
$debug = $this->debug;
if (!$first) {
// disable debug mode on all but the first initialization
$this->debug = false;
}
// will not work with --process-isolation
$first = false;
try {
parent::initializeContainer();
} catch (\Exception $e) {
$this->debug = $debug;
throw $e;
}
$this->debug = $debug;
}

Error when updating datas in the database

I'm trying to solve this little problem.
I have a form that allows to modify datas to a db, but when I click on the submit button(even if it changes the values), it shows an error:
Some mandatory parameters are missing ("name") to generate a URL for route "update_page".
500 Internal Server Error - MissingMandatoryParametersException
Can anybody help me?
This is my controller:
class updateElementController extends Controller
{
public function updateAction($name)
{
$request = $this->get('request');
$ingrediente = $this->getDoctrine()
->getRepository('MyCrudBundle:Elements')
->findOneByName($name);
$em = $this->getDoctrine()->getManager();
$elementsmod = new Elements();
$formupdate = $this->createFormBuilder($elementsmod)
->add('name', 'text')
->add('quantity', 'text')
->add('Change element', 'submit')
->getForm();
$formupdate->handleRequest($request);
if ($formupdate->isValid()) {
$newval = $formupdate->getData();
// change "entity" / object values we want to edit
$namevalue= $newval->getName();
$quantityvalue= $newval->getQuantity();
$element->setName($namevalue);
$element->setQuantity($quantityvalue);
// call to flush that entity manager from which we create $item
$em->flush();
return new RedirectResponse($this->generateUrl('update_page'));
}
return $this->render('MyCrudBundle:Default:index.html.twig', array(
'form' => $formupdate->createView(),
));
}
}
And my route:
update_page:
pattern: /updatePage/{name}
defaults: { _controller: MyCrudBundle:updateElement:update }
(if you need other parts of code, just tell me, I'm a beginner and most certainly it's horribly written)
I suggest two options:
If the name is not mandatory in your routing you can change your routing as below
update_page:
pattern: /updatePage/{name}
defaults: { _controller: MyCrudBundle:updateElement:update, name: null }
If the name is mandatory for the generated url you need to pass the name as the pattern param
if ($formupdate->isValid()) {
$newval = $formupdate->getData();
$namevalue= $newval->getName();
$quantityvalue= $newval->getQuantity();
$element->setName($namevalue);
$element->setQuantity($quantityvalue);
$em->flush();
return new RedirectResponse($this->generateUrl('update_page', array('name' => $name)));
}
base on your query selection you have in your controller the $name is mandatory and I suggest to try the second one

ZF2 - ServiceManager & getServiceConfig problems - unable to fetch or create instance

I am coming across some problems when trying to use ZF2's authentication services. I have to following Module.php getServiceConfig function:
<?php
public function getServiceConfig()
{
return array(
'factories' => array(
'Auth\Model\CustomerTable' => function($sm) {
$tableGateway = $sm->get('CustomerTableGateway');
$table = new CustomerTable($tableGateway);
return $table;
},
'CustomerTableGateway' => function($sm) {
$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
$resultSetPrototype = new ResultSet();
$resultSetPrototype->setArrayObjectPrototype(new Customer()); // prototype pattern implemented.
return new TableGateway('customer', $dbAdapter, null, $resultSetPrototype);
},
'Auth\Model\AuthStorage' => function($sm){
return new \Auth\Model\AuthStorage('jamietech');
},
'AuthService' => function($sm) {
$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
$dbTableAuthAdapter = new DbTableAuthAdapter($dbAdapter,
'customer','username','password');
$authService = new AuthenticationService();
$authService->setAdapter($dbTableAuthAdapter);
$authService->setStorage($sm->get('Auth\Model\AuthStorage'));
return $authService;
},
),
);
}
The AuthStorage factory simply creates a new AuthStorage for us to keep track of the rememberMe function I have, and the AuthService factory creates a new Authentication Service for us. I can't see anything that I have done wrong but when running the following code in the AuthController.php:
<?php
public function loginAction()
{
//if already login, redirect to success page
if ($this->getAuthService()->hasIdentity()){
return $this->redirect()->toRoute('success');
}
$form = new LoginForm();
return array(
'form' => $form,
'messages' => $this->flashmessenger()->getMessages()
);
}
public function logoutAction()
{
$this->getSessionStorage()->forgetMe();
$this->getAuthService()->clearIdentity();
$this->flashmessenger()->addMessage("You have logged out successfully.");
return $this->redirect()->toRoute('auth', array('action'=>'login'));
}
PHPUnit encounters the following errors when running the PHPUnit command:
1: "testLoginActionCanBeAccessed - Zend\ServiceManager\ServiceManager::get was unable to fetch or create an instance of Zend\Db\Adapter\Adapter
1: "testLogoutActionCanBeAccessed - session_regenerate_id(): cannot regenerate session id - headers already sent.
And this error for both login and logout when the -process-isolation command is run:
"Serialization of closure is not allowed in: C;\Users\-----\AppData\Local\Temp\-----
If somebody could help that would be great. I am a ZF noob so try not to be too harsh.
EDIT: BTW THe global.php file includes the service_manager adapter factory illustrated in the ZF2 tutorial application.
Thank you!
Jamie Mclaughlan
did you check these:
autoload_classmap.php (for your module)
in your module.config.php
like this
service_manager' => array(
'aliases' => array(
'mymodule-ZendDbAdapter' => 'Zend\Db\Adapter\Adapter',
),
);
I hope it helps you to find the answer

Resources