special paging requirement in symfony - php

Im triing to create this logic, but i can't reach any goal.
i habe a list of Feeds.It is sometimes veri large almost 2000 entries.
now i want to create a Read function which gives me the first 40 entries and when i have read them or i have scrolled fully to the buttom the next 40 entries should be append at the buttom of the list.
My current staff:
i created a Paging with page and pagesize but the Problem is the following:
let say we have total 20 entries and a begin to request the entries:
page 1 5 items items 0 - 5 of the list (if the ar read they aren't in the list anymore!)
not i load page 2 => items 5-0 aso.. at a point it crosses itself and no items will be retrned!
has anyone an Idea how i can fix this ??
thanks

You must use Paginator class. There is a working example on one of my repositories class:
<?php
namespace Fluency\Bundle\GeneralBundle\Entity\Repository;
use Doctrine\ORM\EntityRepository,
Doctrine\ORM\Tools\Pagination\Paginator;
use Fluency\Bundle\GeneralBundle\Entity\Country;
/**
* CountryRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class CountryRepository extends EntityRepository
{
/**
* #param array $criteria
* #param null $limit
* #param null $start
* #return array with this format (0 => $result, 1 => $counter)
*/
public function findAllByCriteria($criteria = array(), $limit = null, $start = null)
{
$query = $this->getEntityManager()->createQueryBuilder()
->select('c')
->from($this->getEntityName(), 'c')
->orderBy('c.name', 'ASC');
if(null !== $limit)
{
$query->setMaxResults($limit);
}
if(null !== $start)
{
$query->setFirstResult($start);
}
if(!empty($criteria))
{
if($criteria['active'])
{
$query->andWhere($query->expr()->eq('c.active', $criteria['active']));
}
}
$paginator = new Paginator($query);
return array($query->getQuery()->getArrayResult(), $paginator->count());
}
.................
}

Related

Am I doing eager loading correctly? (Eloquent)

I have a method that needs to pull in information from three related models. I have a solution that works but I'm afraid that I'm still running into the N+1 query problem (also looking for solutions on how I can check if I'm eager loading correctly).
The three models are Challenge, Entrant, User.
Challenge Model contains:
/**
* Retrieves the Entrants object associated to the Challenge
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function entrants()
{
return $this->hasMany('App\Entrant');
}
Entrant Model contains:
/**
* Retrieves the Challenge object associated to the Entrant
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function challenge()
{
return $this->belongsTo('App\Challenge', 'challenge_id');
}
/**
* Retrieves the User object associated to the Entrant
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo('App\User', 'user_id');
}
and User model contains:
/**
* Retrieves the Entrants object associated to the User
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function entrants()
{
return $this->hasMany('App\Entrant');
}
The method I am trying to use eager loading looks like this:
/**
* Returns an array of currently running challenges
* with associated entrants and associated users
* #return array
*/
public function liveChallenges()
{
$currentDate = Carbon::now();
$challenges = Challenge::where('end_date', '>', $currentDate)
->with('entrants.user')
->where('start_date', '<', $currentDate)
->where('active', '1')
->get();
$challengesObject = [];
foreach ($challenges as $challenge) {
$entrants = $challenge->entrants->load('user')->sortByDesc('current_total_amount')->all();
$entrantsObject = [];
foreach ($entrants as $entrant) {
$user = $entrant->user;
$entrantsObject[] = [
'entrant' => $entrant,
'user' => $user
];
}
$challengesObject[] = [
'challenge' => $challenge,
'entrants' => $entrantsObject
];
}
return $challengesObject;
}
I feel like I followed what the documentation recommended: https://laravel.com/docs/5.5/eloquent-relationships#eager-loading
but not to sure how to check to make sure I'm not making N+1 queries opposed to just 2. Any tips or suggestions to the code are welcome, along with methods to check that eager loading is working correctly.
Use Laravel Debugbar to check queries your Laravel application is creating for each request.
Your Eloquent query should generate just 3 raw SQL queries and you need to make sure this line doesn't generate N additional queries:
$entrants = $challenge->entrants->load('user')->sortByDesc('current_total_amount')->all()
when you do ->with('entrants.user') it loads both the entrants and the user once you get to ->get(). When you do ->load('user') it runs another query to get the user. but you don't need to do this since you already pulled it when you ran ->with('entrants.user').
If you use ->loadMissing('user') instead of ->load('user') it should prevent the redundant call.
But, if you leverage Collection methods you can get away with just running the 1 query at the beginning where you declared $challenges:
foreach ($challenges as $challenge) {
// at this point, $challenge->entrants is a Collection because you already eager-loaded it
$entrants = $challenge->entrants->sortByDesc('current_total_amount');
// etc...
You don't need to use ->load('user') because $challenge->entrants is already populated with entrants and the related users. so you can just leverage the Collection method ->sortByDesc() to sort the list in php.
also, You don't need to run ->all() because that would convert it into an array of models (you can keep it as a collection of models and still foreach it).

How can I handle a shopping basket where there are a lot of products with potentially different cost calculation

I was doing an assignment and I had at least few products where I had a problem such as buy one get one free, buy 4 get one free and buy 5 and get 20 percent off. Now I was wondering what should I do to handle such a problem because as I encounter more products the code base for the product calculation will go out of hand. What is the best solution here ? Currently what I have done is to have a single class based on each product code and the calculation for the product is local to that file. The object construction is done via static factory method. Any suggestions with regards to the algorithm improvement and if I can further reduce the code and make it simple will be great.
class BOGOFCalculator implements BasketPriceCalculator
{
const MIN_ITEMS_REQUIRED_FOR_DISCOUNT = 2;
/**
* #param Product $product
* #param integer $totalItems
* #return float
*/
public function calculate(Product $product, $totalItems): float
{
return $this->calculateFullPrice($product, $totalItems) - $this->calculateDiscount($product, $totalItems);
}
/**
* #param Product $product
* #param $totalItems
* #return float
*/
private function calculateFullPrice(Product $product, $totalItems): float
{
return $product->getPrice() * $totalItems;
}
/**
* #param Product $product
* #param $totalItems
* #return float
*/
private function calculateDiscount(Product $product, $totalItems): float
{
return $product->getPrice() * floor($totalItems/static::MIN_ITEMS_REQUIRED_FOR_DISCOUNT);
}
}
The basket looks like below
class Basket
{
/**
* #param Product[] $products
*
* #return float
*/
public function calculateProductTotal(Product ...$products): float
{
$totalItemsOfEachProduct = $this->getTotalItemsOfEachProductInTheBasket(...$products);
$items = $this->getDistinctProductsInTheBasketWithQuantity(...$products);
$total = 0;
foreach ($items as $productCode => $item) {
/** #var BasketPriceCalculator $priceCalculator */
$priceCalculator = PriceCalculatorFactory::getInstance($productCode);
$total += $priceCalculator->calculate($item, $totalItemsOfEachProduct[$productCode]);
}
return $total;
}
/**
* #param Product[] $products
*
* #return array
*/
private function getTotalItemsOfEachProductInTheBasket(Product ...$products): array
{
$totalItemsPerProductCode = array_map(function ($product) { return $product->getCode(); }, $products);
return array_count_values($totalItemsPerProductCode);
}
/**
* #param Product[] $products
*
* #return array
*/
private function getDistinctProductsInTheBasketWithQuantity(Product ...$products): array
{
$items = [];
foreach ($products as $product) {
if (!array_key_exists($product->getCode(), $items)) {
$items[$product->getCode()] = $product;
}
}
return $items;
}
}
As I see it, there are two separate concepts here: discounted products (like -30% with no custom conditions) and deals. Which would mean, that your basket contains collection of products and collection of deals.
Each time to "process" the basket (calling on it getTotalPrice(), listProducts() or getTotalSaving() you apply the deals to the products. That "processing" should not affect the basket's internal collection of products, but only the returned collection/result.
Every deal contains two aspects: condition (or rule) and reward. You would use the rule to determine, whether you should add a given deal to user's basket.
public function addProduct($product)
{
for ($this->deals as $deal) {
if ($this->basket->willMatch($product, $deal) {
$this->basket->addDeal($deal);
}
}
$this->basket->addProduct($product);
}
When checking whether a deal needs to be added, you need to compare the "real list" of products, that already are inside the basket, plus the "incoming item" against the rule of a deal.
As for the addition of any deal to the basket, you just need to make sure, that all deals inside a basket are unique.
When you remove an item from a basket the basket should recheck if rules for all deals still match and discard the obsolete ones.
And when you are requesting to list of *products from a user's basket, you apply the reward function of all the deals, where each returns a new list of products without affecting the baskets "real list".
Note: It would also mean, that you need to actually clone the products, since otherwise you can't use the reward to apply discounts (because otherwise the discounts would also get applied to the "real list" items due to pass-by-handler behavior)
This way, the deals get applied without needing to recheck whether they actually match the rules or not. You just run the reward which, for example, add one free USB cable for every charger and 2 cables in the listProducts() result.
As for how you specifically define the deals, it's up to you. They can be either made from your sales CMS or can be hardcoded as distinct classes or you can mix them. It does not really affect the above described approach.
Update
So, the example of a "hardcoded deal" with conditions, that I mentioned above (the one about charger):
namespace Market\Deal;
use Market\Repository\Product as Stock;
use Market\Entity\Product;
use Market\Entity\ProductCollection;
class ChargetExtras implements Applicable
{
private $repository;
public function __construct(Stock $repsoitory)
{
$this->repository = $repository;
}
public function canApply(Product $next, ProductCollection $current): bool
{
$predicted = new ProductCollection;
$predicted->copyFrom($current);
$predicted->add($next);
return $predicted->has(new Product(1515)) >= 2 &&
$predicted->has(new Product(48)) >= 1;
// here 1515 is ID for cables and 48 - for charger
}
public function apply(ProductCollection $current): ProductCollection
{
$next = new PdocutCollectionl
$next->copyFrom($current);
$count = min(
(int) $current->has(new Product(1515))/2,
$current->has(new Product(48))
);
// for each combination of 2 cables and 1 charger, add free cable
while ($count--) {
$gratis = $this->repository->get(1515);
$gratis->setPrice(0);
$next->add($gratis);
}
return $next;
}
}
The canApply() method is the rule which is used for checking, whether the deal can be applied and the reward is contained in apply() method.
You would call them both from within the Basket class, by passing the "real list" of products. As you can see, at no point the deal actually affects that original list. It always works on a copy.
And, since the logic in each deal is relatively simple, you can create some "dynamic rule" class, that uses conditions, that were defined in your CMS.

Get Record Localization within backend module

I'm struggling with the TYPO3 l10n and the modifying of localized records.
Short Question:
How can I get the localized record from my extbase model?
In more detail:
I am using a backend module to modify multiple records at the same time. At the moment it only works for origin records. But the customer wants to use this module to edit localized records also.
This is what I tryed so far:
An array is passing the origin uid's to the repository class. Depending on the SysLanguageUid I am doing a findByUid if its an origin record and if the SysLanguageUid is anything higher than 0 I do the following query:
protected function findByUidAndSysLanguageUid($uid, $sysLanguageUid) {
$query = $this->createQuery();
$query->matching(
$query->equals('l10n_parent', $uid),
$query->equals('sys_language_uid', $sysLanguageUid)
);
return $query->execute();
}
This query works fine for the first record. But what really confuses me is, ongoing from the second entry the query returns the origin records (even while the sys_language_uid in the query is set to >0).
Any ideas how to handle this?
PS: If you need some more information then let me know it.
UPDATE:
So far I managed it to get the raw query from the above constraint:
Query of the first record:
SELECT tx_extkey_domain_model_mymodel.*
FROM tx_extkey_domain_model_mymodel
WHERE (tx_extkey_domain_model_mymodel.l10n_parent = '133' AND tx_extkey_domain_model_mymodel.sys_language_uid = '1') AND
(tx_extkey_domain_model_mymodel.sys_language_uid IN (1, -1) OR
(tx_extkey_domain_model_mymodel.sys_language_uid = 0 AND
tx_extkey_domain_model_mymodel.uid NOT IN (SELECT tx_extkey_domain_model_mymodel.l10n_parent
FROM tx_extkey_domain_model_mymodel
WHERE tx_extkey_domain_model_mymodel.l10n_parent > 0 AND
tx_extkey_domain_model_mymodel.sys_language_uid = 1 AND
tx_extkey_domain_model_mymodel.deleted = 0))) AND
tx_extkey_domain_model_mymodel.hidden = 0 AND (tx_extkey_domain_model_mymodel.starttime 1479390060) AND
tx_extkey_domain_model_mymodel.deleted = 0
ORDER BY tx_extkey_domain_model_mymodel.name ASC
LIMIT 1;
Query of the second record:
SELECT tx_extkey_domain_model_mymodel.*
FROM tx_extkey_domain_model_mymodel
WHERE (tx_extkey_domain_model_mymodel.l10n_parent = '134' AND tx_extkey_domain_model_mymodel.sys_language_uid = '1') AND
(tx_extkey_domain_model_mymodel.sys_language_uid IN (1, -1) OR
(tx_extkey_domain_model_mymodel.sys_language_uid = 0 AND
tx_extkey_domain_model_mymodel.uid NOT IN (SELECT tx_extkey_domain_model_mymodel.l10n_parent
FROM tx_extkey_domain_model_mymodel
WHERE tx_extkey_domain_model_mymodel.l10n_parent > 0 AND
tx_extkey_domain_model_mymodel.sys_language_uid = 1 AND
tx_extkey_domain_model_mymodel.deleted = 0))) AND
tx_extkey_domain_model_mymodel.hidden = 0 AND (tx_extkey_domain_model_mymodel.starttime 1479390360) AND
tx_extkey_domain_model_mymodel.deleted = 0
ORDER BY tx_extkey_domain_model_mymodel.name ASC
LIMIT 1;
UPDATE 2
This now confuses me even more...
I put both of the sql queries into heidisql and run them manually. They work perfectly!
So it seems like there is no problem with the query itself.
UPDATE 3
This is the method of the repository which gets called by the controller.
/**
* #param array $parentUidCollection
* #param int $L
*/
protected function updateByCollection(array $parentUidCollection, $L = 0) {
//$L is the language $_GET parameter. cant use TSFE because of inside of a backend module
if($L > 0) {
$this->setTempQuerySettings($L);
}
foreach ($parentUidCollection as $parentUid){
$myModel = $this->findTranslatedByParentId($parentUid)->getFirst();
$myModel->setDescription('foo');
$this->update($myModel);
}
}
My defaultQuerySettings are overwritten in the third line if the actual language is not the default language.
/**
* #param $sysLanguageUid
*/
protected function setTempQuerySettings($sysLanguageUid) {
/** #var \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings $tempQuerySettings */
$this->originalQuerySettings = $this->objectManager->get('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Typo3QuerySettings');
$tempQuerySettings = clone $this->originalQuerySettings;
$tempQuerySettings->setRespectStoragePage(false);
$tempQuerySettings->setRespectSysLanguage(true);
$tempQuerySettings->setLanguageUid($sysLanguageUid);
$tempQuerySettings->setLanguageMode(false);
$tempQuerySettings->setLanguageOverlayMode(false);
$this->setDefaultQuerySettings($tempQuerySettings);
}
And now with the function suggessted by Toke Herkild but without the query settings inside. they are set in the above snipped.
/**
* #param int|string $parentUid
* #return array|\TYPO3\CMS\Extbase\Persistence\QueryResultInterface
*/
public function findTranslatedByParentId($parentUid)
{
$query = $this->createQuery();
$query->matching($query->equals('l10n_parent', $parentUid));
return $query->execute();
}
UPDATE 4:
After executing the code the database looks like this:
The 100 uid's are the origin and the 200 are the localized records in this picture.
NOTICE: Below solution would work except for this bug:
https://forge.typo3.org/issues/47192
Maybe just make it simple, inside your ModelRepository do something like:
public function findTranslatedByParentId($parentUid) {
$query = $this->createQuery()
$qrySettings = $query->getQuerySettings();
$qrySettings->setLanguageMode('ignore');
$qrySettings->setLanguageOverlay(FALSE);
$query->setDefaultQuerySettings($qrySettings);
return $query->matching($query->equals('l18n_parent', $parentUid))->execute();
}
You need to disable the persistence layers language handling or it believes you try to fetch the localized version of the record for your current sys_language.

Laravel 5 - Finding the pagination page for a model

I am working on building a basic forum (inspired by laracasts.com/discuss). When a user posts a reply to a thread:
I'd like to direct them to the end of the list of paginated replies with their reply's anchor (same behavior as Laracasts).
I'd also like to return the user to the correct page when they edit one of their replies.
How can I figure out which page a new reply will be posted on (?page=x) and how can I return to the correct page after a reply has been edited? Or, from the main post listing, which page the latest reply is on?
Here is my current ForumPost model (minus a few unrelated things) -
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
/**
* Class ForumPost
*
* Forum Posts table
*
* #package App
*/
class ForumPost extends Model {
/**
* Post has many Replies
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function replies()
{
return $this->hasMany('App\ForumReply');
}
/**
* Get the latest reply for a post
* #return null
*/
public function latestReply()
{
return $this->replies()->orderBy('created_at', 'desc')->first();
}
}
UPDATE
Take a look at this and let me know what you think. It's a bit weird in how it works but it's returning the correct page for a given reply ID and it's just one method:
public function getReplyPage($replyId = null, $paginate = 2)
{
$id = $replyId ? $replyId : $this->latestReply()->id;
$count = $this->replies()->where('id', '<', $id)->count();
$page = 1; // Starting with one page
// Counter - when we reach the number provided in $paginate, we start a new page
$offset = 0;
for ($i = 0; $i < $count; $i++) {
$offset++;
if ($offset == $paginate) {
$page++;
$offset = 0;
}
}
return $page;
}
Fundamentally you are working with two values: first, what the index of a reply is in relation to all the replies of a post, and second the number of replies in on a page.
For example, you might have a reply with an id of 301. However, it is the 21st reply on a specific post. You need to some way to figure out that it is the 21st reply. This is actually relatively simple: you just count how many replies are associated with that post but have smaller ids.
//get the number of replies before the one you're looking for
public function getReplyIndex($replyId)
{
$count = $this->replies()->where('id', '<', $replyId)->count();
return $count;
}
That method should return the index of the reply you are looking for based- assuming, of course, that your replies are using auto-increment ids.
The second piece of the puzzle is figuring out which page you need. This is done using integer division. Basically you just divide the number normally, but don't use the remainder. If you are looking at the 21st reply, and you have 10 replies to a page, you know it should be on the third page (page 1: 1-10, page 2: 11-20, page 3: 21-30). This means you need to integer divide your reply index by your replies-per-page, then add 1. This will give us 21/10+1, which, using integer division, gives us 3. Yay!
//divides where we are by the number of replies on a page and adds 1
public function getPageNumber($index, $repliesPerPage)
{
$pageNumber = (int) ($index/$repliesPerPage+1);
return $pageNumber;
}
Alright, so now you just need to pull that page. This simply requires a method where you specify what page number you need, and how many replies to a page there are. That method can then calculate the offset and the limit, and retrieve the records you need.
public function getPageOfReplies($pageNumber, $repliesPerPage)
{
$pageOfReplies = $this->replies()->offset($pageNumber*$repliesPerPage)->limit($repliesPerPage)->get();
return $pageOfReplies;
}
For good measure, though, we can build a method to get the index of the final reply.
public function getLastReplyIndex()
{
$count = $this->replies()->count();
return $count;
}
Great! Now we have all the building blocks we need. We can build some simple methods that use our more general-purpose ones to easily retrieve the data we need.
Let's start with a method that gets the entire page of replies on which a single reply resides (feel free to change the names (also I'm assuming there are 10 replies per page)):
public function getPageThatReplyIsOn($replyId)
{
$repliesPerPage = 10;
$index = $this->getReplyIndex($replyId);
$pageNumber = $this->getPageNumber($index, $repliesPerPage);
return $this->getPageOfReplies($pageNumber, $repliesPerPage);
}
For good measure, we can make a method that gets the page of final replies.
public function getFinalReplyPage()
{
$repliesPerPage = 10;
$index = $this->getLastReplyIndex();
$pageNumber = $this->getPageNumber($index, $repliesPerPage);
return $this->getPageOfReplies($pageNumber, $repliesPerPage);
}
You could build a variety of other methods to use our building block methods and jump around pages, get the pages after or before a reply, etc.
A couple notes
These all go in your ForumPost model, which should have a one-to-many relationship with your replies.
These are a variety of methods that are meant to provide a wide array of functionality. Don't be afraid to read through them and test them individually to see exactly what they are doing. None of them are very long, so it shouldn't be difficult to do that.
Here is what I came up with. If anyone has any suggestions to improve on this, PLEASE let me know. I'm really wondering if there is a more Laravel way to do this and I would really appreciate Jeffrey Way sharing his secret, since he is doing this exact thing over at Laracasts.
/**
* Get Reply Page
*
* Returns the page number where a reply resides as it relates to pagination
*
* #param null $replyId Optional ID for specific reply
* #param bool $pageLink If True, return a GET parameter ?page=x
* #param int $paginate Number of posts per page
* #return int|null|string // Int for only the page number, null if no replies, String if $pageLink == true
*/
public function getReplyPage($replyId = null, $pageLink = false, $paginate = 20)
{
// Find the page for a specific reply if provided, otherwise find the most
// recent post's ID. If there are no replies, return null and exit.
if (!$replyId) {
$latestReply = $this->latestReply();
if ($latestReply) {
$replyId = $latestReply->id;
} else {
return null;
}
}
// Find the number of replies to this Post prior to the one in question
$count = $this->replies()->where('id', '<', $replyId)->count();
$page = CEIL($count / $paginate +1);
// Return either the integer or URL parameter
return $pageLink ? "?page=$page" : $page;
}

Working with a MY_Model

I’m using jamie Rumbelow’s MY model as a way to better deal with my application.
https://github.com/jamierumbelow/codeigniter-base-model
The MY_model is the same except I have an added in variable for defining whether or not an item in the db is marked as being soft deleted or not.
protected $soft_delete_value = 3;
I only have that variable defined and have not altered his code yet to account for this value.
I have two things I want to do with this titles model that I need help understanding.
Titles Table - title_id, title_name, title_status_id
Title_Statuses_Table - title_status_id, title_status_name
What I want it to do is retrieve all of the rows that have a title_status_id of 1 and 2 and 3 because the soft delete value is different than the default set in the MY Model. What I would also like to have is instead of it returning the integer have it return the name of the status.
Expected results:
An array of objects that contain a title_id, title_name, title_status_name for which the titles have a status id of 1,2, or 3.
Testing
$titles = $this->titles_model->get_all();
echo "<pre>";
print_r($titles);
echo "</pre>";
Actual results:
SELECT *
FROM (`titles`)
WHERE `title_status_id` = 0
<pre>Array
(
)
My Code
class Titles_model extends MY_Model
{
/* --------------------------------------------------------------
* VARIABLES
* ------------------------------------------------------------ */
/**
* This model's default database table.
*/
public $_table = 'titles';
public $primary_key = 'title_id';
/**
* Support for soft deletes and this model's 'deleted' key
*/
public $soft_delete = TRUE;
public $soft_delete_key = 'title_status_id';
public $soft_delete_value = 4;
public $_temporary_with_deleted = FALSE;
public function __construct()
{
parent::__construct();
}
}
Anybody else have any additional ideas/suggestions?
EDIT:
I've been tryign to figure this out all day and have hit a dead end.
well here would be the function that you would need to get your expected result
$this->db->select('
titles.*,
status.*,
')
->join('status s', 'titles.title_status_id = s.title_status_id', 'LEFT')
->where('titles.title_status_id', 1)
->or_where('titles.title_status_id', 2)
->or_where('titles.title_status_id', 3)
->from('titles')
->get()
->result_object();

Categories