I am trying to create a random slug based on a dictionary of about ~100 words. I have come up with the following solution but I have been thinking about it too long and can't tell if it is efficient and cannot figure out how to properly test it.
class SlugModel
{
/**
* Get random words from the slug_words table
* #param int $limit how many words should be fetched
* #return array the array of random words
*/
public static function getRandomSlugWords($limit)
{
// returns random words in an array. Ex:
return array('pop', 'funk', 'bass', 'electro');
}
/**
* Generate a random slug based on contents of slug_words
* #return str the slug
*/
public static function generateSlug($limit=4)
{
do {
$words = self::getRandomSlugWords($limit);
$slugified = implode('-', $words);
if(PlaylistModel::doesSlugAlreadyExist($slugified)) // returns true or false
{
// try a few different combinations before requesting new words from database
for ($i=0; $i < $limit; $i++)
{
$words[] = array_shift($words); // take the first and shift it to the end
$slugified = implode('-', $words);
if(!PlaylistModel::doesSlugAlreadyExist($slugified)) // break only if it does NOT exist
break;
}
}
} while (PlaylistModel::doesSlugAlreadyExist($slugified));
return $slugified;
}
}
I think this code works but I also think it can be made more efficient or I may be overthinking it. I could also have it as simple as
do {
$words = self::getRandomSlugWords($limit);
$slugified = implode('-', $words);
} while (PlaylistModel::doesSlugAlreadyExist($slugified));
But I am trying to test different combinations of the same words before pinging the database with another request for different words (I am using RAND() to get randomized results and trying to minimize this).
Any insight is appreciated! Thank you!
I think doing a single query for getting limit+N random words is a better (faster, resource friendly) approach. For example, whenever we need a slug made up from 4 elements, let us select 8 words.
SELECT slug FROM `slugs` ORDER BY RAND() LIMIT 8;
In this case, we will shuffle an array containing twice the items we will use - there are 24 permutations of 4 by 4, while there are 1680 permutations of 8 by 4 -, leaving us with more probable valid slugs with less queries.
In that case, we would have a
public static function getRandomSlugs($limit)
{
shuffle(self::$slugs); // 8 words from database
return array_slice(self::$slugs, 0, $limit); // we only need 4
}
and make our way in using a
public function generateSlug($limit = 4)
{
do {
$slugs = self::getRandomSlugs($limit); // Get N slugs
$mySlug = implode('-', $slugs); // Join the elements
} while ($this->slugExists($mySlug));
return $mySlug;
}
Checking if a slug is already taken can be done in a couple ways; selecting from database where the column is your slug, or setting up the table as unique and checking the return on inserting the data.
Anyhow, remember to keep the methods short, precise, and clean.
Related
I am building an Laravel application to generate sales invoices.
Multiple people from multiple companies generate invoices at a time.
I have a table for Invoices as invoice_table where id is primary key and number is unique
id | company_id | series | number | amount
Now to generate invoice number what I do is; for a particular company take count and add 1 to that.
//assume $inv->prefix is a string which gives me series for particular company.
$series = Invoice::where('series', $inv->prefix)->max('number');
if(isset($series) && strlen($series) > 0){
$series += 1;
} else {
$series = 1;
}
$inv->number = $series;
Now the problem is when two users tries to generate invoice for same series at a time it give me duplication error as number column in my table is unique.
Can i do something like
do{
$series = Invoice::where('series', $inv->prefix)->max('number');
if(isset($series) && strlen($series) > 0){
$series += 1;
} else {
$series = 1;
}
$inv->number = $series;
} while($inv->save())
Can anyone help me here.
If i get duplicate entry exception the code should bring the count again and it should try to save the record again.
I will suggest don't just increment the number for new InvoiceId, make a pattern for that ex-
Create a utility class -
class NumberUtility
{
/**
* This method generates the random number
*
* #param int $length
*
* #return int
*/
public static function getRandomNumber($length = 8): int
{
$intMin = (10 ** $length) / 10;
$intMax = (10 ** $length) - 1;
try {
$randomNumber = random_int($intMin, $intMax);
} catch (\Exception $exception) {
\Log::error('Failed to generate random number Retrying...');
\Log::debug(' Error: '.$exception->getMessage());
$randomNumber = self::getRandomNumber($length);
}
return $randomNumber;
}
}
Now create a method to get unique invoice number as below -
public function getUniqueInvoiceNumber($companyId)
{
$randomNumber = NumberUtility::getRandomNumber(4);
$invoiceNumber = (string)$companyId.$randomNumber;
$exist = Invoice::where('number', $invoiceNumber)->first();
if ($exist) {
$invoiceNumber = $this->getUniqueInvoiceNumber($companyId);
}
return $invoiceNumber;
}
You could use microtime to handle your invoices. Chances of having duplicated records are very low; though for design's sake I'd choose tables locking (see the whole answer)
$inv->number = str_replace(' ', '', microtime());
$inv->save();
Multiple people from multiple companies generate invoices at a time.
Randomizing data or incrementing it is not the way to go in this is the case. If you are generating the invoices based on a custom pattern, which uses as variables data from database, chances of generating duplicates are high. You should locks the tables you're working with for the current transaction. This will prevent any duplicated data from being inserted. I don't know your entire database structure though I'm offering a demo:
LOCK TABLES working_table WRITE, working_table2 WRITE;
INSERT INTO working_table ( ... )
VALUES ( ...,
(SELECT number
FROM working_table2
WHERE series = 'prefix'
ORDERBY number DESC
LIMIT 1)+1
);
UNLOCK TABLES;
You can then call the sql statement like this:
DB::select("
LOCK TABLES working_table WRITE, working_table2 WRITE;
INSERT INTO working_table ( column )
VALUES (
(SELECT number
FROM working_table2
WHERE series = ?
ORDERBY number DESC
LIMIT 1)+1
);
UNLOCK TABLES;
", [ 'param1' ]) // these are the parameters to prepare
Note: If you are using persistent database connections you must handle all thrown errors from you Laravel application, because unhandled errors will halt the script without halting the child child process which keeps the connection to the database alive. Unhalting the child process keeps the tables locked! as your database is locking them for connection session. Please read this post for detalied information regarding this behaviour.
You can use uniqid() function of PHP:
$series = uniqid(); //current time in microseconds
Trying to hack apart an annoying bug here.
We have a regex which correctly strips off leading articles like "a", "the", etc. It also sorts alphabetically fine for some of the entries, but not for the rest. What happens is we get a list where some of the listings with these articles are embedded properly within the output, and then it gets to the end and starts over, with those entries correctly sorted among themselves.
Here's an example of how it's appearing
Apple
Banana
The Carrot
The Grapefruit
Kiwi
The aardvark
The emu
A zebra
As you can see, it does fine at first, but then gets to the end and starts over with only those items with articles. I've looked at the data, and can't find any consistencies that would explain the variation.
Here's the regex comparison (in an entity called Bibliography)
public static function compareTitles(Bibliography $a, Bibliography $b)
{
$ignored_word_regex = '/^(the|a|an)\s/i';
$tag_quote_regex = '/"?(\<\/?[a-zA-Z]+\>)?/';
$a_test = preg_replace($ignored_word_regex ,'', $a->getTitle());
$a_test = preg_replace($tag_quote_regex, '', $a_test);
$b_test = preg_replace($ignored_word_regex ,'', $b->getTitle());
$b_test = preg_replace($tag_quote_regex, '', $b_test);
return strcmp( $a_test,
$b_test);
}
This is being pulled in from an entity called Review
/**
* #param Review $a
* #param Review $b
*
* #return boolean
*/
public static function compareTitles($a, $b)
{
return \Entity\Bibliographies\Bibliography::compareTitles($a->getBiblio(), $b->getBiblio());
}
This is then sorted in the controller using these two methods:
public function exportIndexTitleAction( Request $request ){
$response = $this->renderExportXMLSorted(
'Review:Export/index_title.xml.twig',
$request,
array('Entity\Reviews\Review', 'compareTitles'));
return $this->setExportHeaders($response, date("o-m-d-")."index_title.txt");
}
and using a uasort:
protected function renderExportXMLSorted($template, Request $request, $sort_method, $filter_method = null){
$reviews = $this->getReviewsFromRequest($request);
if ($filter_method != null)
$reviews = array_filter($reviews, $filter_method);
uasort($reviews, $sort_method);
return $this->renderExportXML($template, $reviews);
}
I've been digging through this and cannot figure what causes it to stop and start over again. I can get it to sort fine if it treats "the" and "a" as they are, but they really need to be stripped, but also be displayed. (yes, it would be easier to simply remove them, but they need to remain).
I'm thinking there's something in the regex that I'm missing...
Obviously the problem is case. You need to use strcasecmp to treat both cases the same. You could also strtolower or strtoupper both before the compare.
Also, that is a lot of code that can be boiled down to something simpler:
uasort($array, function($a, $b) {
$p = '/^(the|a|an)\s/i';
$a = preg_replace($p, '', $a);
$b = preg_replace($p, '', $b);
return strcasecmp($a, $b);
});
I am currently working on a Laravel application and it has a feature where some certain users will login to generate unique, 12-digit PIN numbers.
Users with certain privileges should be able to generate a minimum of 1,000 or 10,000 PINS in one request.
I am presently doing this, which is inefficient, as it's very costly in terms of server resources:
class PinController extends ApiController
{
public function __construct(Auth $auth, Pin $pin){
$this->auth = $auth;
$this->pin = $pin;
$this->middleware('jwt.auth', ['except' => ['index','show']]);
}
//pin generator - works
protected function random($length = 14)
{
$pool = '123456789';
return substr(str_shuffle(str_repeat($pool, $length)), 0, $length - 2);
}
public function store(StoreRequest $request)
{
$number_of_pins = $request->get('number_of_pins');//say 10000
$user = $this->auth->user();
$numbers =[];
for($i=0; $i <= $number_of_pins; $i++){
$number = $this->random(); //generate random 12 digit pins
$numbers[] = $number;
if(!in_array($numbers, $number) || !$this->pin->where('number',$number)->get())
{
$this->pin->create(['number'=>$number,'user_id'=>$user->id]);
}else{
$i--;
}
}
}
}
This takes a very long time to execute and fails all the time.
I am considering using queue jobs to do this in chunks, but it might not be the best solution for my app's feature.
How can I do this in a way that doesn't take so long and doesn't fail?
if(!in_array($numbers, $number) for testing the uniqueness will get slower and slower as your $numbers list grows..... rather than setting the $number as a value in the array, set it as the key, and then you can do a direct key check for duplication which is much faster than using in_array()
You're also adding $number to $numbers before doing the duplicates check, so you've already added the duplicate to your array (which is why it always fails)
The database check for every value is also a big overhead, best eliminated if you can do so
if(!isset($numbers[$number])) {
$this->pin->create(['number'=>$number,'user_id'=>$user->id]);
$numbers[$number] = true;
}else{
$i--;
}
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;
}
This is a pretty broad question but I'm hoping I could get a bit of guidance.
I'm building a reporting system for my company. I have classes for Customer, Order, Invoice, and Item. They work great for individual objects. However, this is a reporting system and I will need to query and summarize these objects in a variety of ways.
For example, a single Order object will have a total dollar value for that one order. But if I'm generating a report for the month, I want to summarize a group of orders that match whatever parameters I pass my query (such as a date range and/or customer number).
This typically involves additional work such as accumulating running totals for month to date or year to date comparisons. This is where things get a little fuzzy for me. Does that logic belong in the Order class? If not, then where? Note I will also have to do the same for my Invoice class.
Here's a simplified version of what I'm doing now with my Order class. I use one function (getOrders) that returns an array of Order objects, and another function (getOrderGroup) that returns an array of grouped results (not objects).
It's the getOrdersGroup() function I'm most unclear about. If there is a better practice for reporting on grouped results, along with counts, sums and running totals, please point me down the better path!
<?php
class Order {
public $number;
public $customer;
public $date_ordered;
public $date_shipped;
public $salesperson;
public $total;
public function __construct(array $data = array()) {
$this->number = $data['number'];
$this->customer = $data['customer'];
$this->date_ordered = $data['date_ordered'];
$this->date_shipped = $data['date_shipped'];
$this->salesperson = $data['salesperson'];
$this->total = $data['total'];
}
/**
* Returns an array of order objects
*/
public static function getOrders(array $options = array()) {
$orders = array();
// Build query to return one or more orders
// $options parameter used to form SQL query
// ......
$result = mysql_query($query);
while ($row = mysql_fetch_array($result, MYSQL_ASSOC)) {
$order = new Order($row);
$orders[] = $order;
}
return $orders;
}
/**
* Returns an array of grouped order results (not objects)
*/
public static function getOrdersGroup(array $options = array()) {
$group = array();
// Build query that contains COUNT() and SUM() group by functions
// ......
$running_total = 0;
$result = mysql_query($query);
while ($row = mysql_fetch_array($result, MYSQL_ASSOC)) {
// Accumulate running total for the month
$running_total += $row['sum_total'];
// Build the group array with a listing of summarized data
// Note: The order class is never actually instantiated here
// Also, in this example we're grouping by date_ordered...
// but in reality the result should be able to be grouped by a
// dynamic field, such as by customer, or by salesperson,
// or a more detailed breakdown with year, month, day and a running
// total break at each level
$group[] = array(
"date_ordered" => $row["date_ordered"],
"count_customers" => $row["count_customers"],
"count_orders" => $row["count_orders"],
"count_salespersons" => $row["count_salesperson"],
"sum_total" => $row["sum_total"],
"running_total" => $running_total);
}
return $group;
}
/**
* Queries to get ordered items if drilling down to item level...
*/
public function getItems() {
// blah
}
}
?>
There are a number of tasks to be done and, yes, some of them are specific to each class. Especially when it comes to knowing which columns there are and how to treat them. But on the other hand there is the abstract logic of 'grouping something' and of building search patterns (and this includes of course also a range of date handling functions). These two can/should be placed in separate objects/functions which can be called from both classes, orders and invoices.
A similar thing goes for putting together the result arrays. Here we need to be able to adapt to all sorts of different grouping conditions. The column names are class specific but the general logic can be put in separate classes or functions.