Building Web Apps with Laravel Module 11 – Inside the framework
Collections, strings, numbers and dates
Turn rows into reports with Laravel collections, format strings and numbers for Indian readers, and keep dates right across time zones with Carbon.
What you will learn
- Transform rows into a report with collection methods such as filter, map, groupBy, keyBy and reduce
- Use lazy collections and cursors to process large inputs in little memory
- Format strings, numbers and currency for Indian readers with Str and Number
- Handle dates and time zones with Carbon, storing UTC and showing India's time
Before you start
What you’ll learn · Before you start
What you will learn
- Transform rows into a report with collection methods such as filter, map, groupBy, keyBy and reduce
- Use lazy collections and cursors to process large inputs in little memory
- Format strings, numbers and currency for Indian readers with Str and Number
- Handle dates and time zones with Carbon, storing UTC and showing India's time
Before you start
On this page
Pathshala’s accountant wants April’s fees per batch from 12 instalment rows, each amount in Indian digit grouping such as 1,23,456.78, and each payment time shown in India although the server stored it in UTC. Laravel ships a tool for each part of that job: collections for lists, Str for strings, Number for numbers and Carbon for dates.
This lesson builds the fee report with collections, formats its strings and numbers for Indian readers, and shows its dates in India’s time. Later parts stream inputs too large for memory, write the short Indian forms that Number lacks, and test the day boundaries that a UTC timestamp crosses.
Collections turn rows into a report, one method at a time
A collection: A Laravel object that wraps an array and offers chainable methods such as filter, map, groupBy and sum, each returning a new collection. collect() makes one, and Eloquent queries return them. wraps an array in an object whose chaining methods each return a new collection, so a report reads top to bottom as the steps it takes. collect($rows) makes one, and Eloquent queries return them too. The run builds Pathshala’s report from 12 rows:
report/report_probe.php
<?php
// Turns Pathshala's instalment rows into a fee report with one collection pipeline, printing what each step gives.
require __DIR__.'/vendor/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$rupees = fn (int $paise) => number_format($paise / 100, 2);
$rows = collect(require __DIR__.'/instalments.php');
echo 'rows: '.$rows->count().PHP_EOL;
// 1. Keep the rows that were paid, then add a month key to each.
$paid = $rows->filter(fn (array $row) => $row['status'] === 'paid')
->map(fn (array $row) => $row + ['month' => substr($row['due_on'], 0, 7)]);
echo 'paid rows: '.$paid->count().PHP_EOL;
// 2. Group by batch, then by month, and total each group.
$report = $paid->groupBy(['batch', 'month'])
->map(fn ($months) => $months->map(fn ($group) => $group->sum('amount_paise')));
echo PHP_EOL.'collected, by batch and month (rupees):'.PHP_EOL;
foreach ($report as $batch => $months) {
foreach ($months as $month => $paise) {
echo ' '.str_pad($batch, 16).$month.' '.str_pad($rupees($paise), 9, ' ', STR_PAD_LEFT).PHP_EOL;
}
}
// 3. One total per batch, with reduce.
$byBatch = $paid->groupBy('batch')->map(fn ($group) => $group->reduce(fn (int $sum, array $row) => $sum + $row['amount_paise'], 0));
echo PHP_EOL.'collected per batch: '.$byBatch->map($rupees)->toJson().PHP_EOL;
// 4. Who owes money: overdue rows keyed by student, with their totals.
$overdue = $rows->where('status', 'overdue')->groupBy('student')->map->sum('amount_paise');
echo 'overdue per student: '.$overdue->map($rupees)->toJson().PHP_EOL;
// 5. keyBy keeps the LAST row for each key.
echo 'keyBy(student) on 12 rows keeps '.$rows->keyBy('student')->count().' rows, Asha\'s due_on: '.$rows->keyBy('student')['Asha Verma']['due_on'].PHP_EOL; report/instalments.php
<?php
// Twelve fee instalments of Pathshala's two batches for April to June 2031: invented students, amounts in paise.
return [
['batch' => 'Maths Morning', 'student' => 'Asha Verma', 'due_on' => '2031-04-05', 'amount_paise' => 118000, 'status' => 'paid'],
['batch' => 'Maths Morning', 'student' => 'Kabir Singh', 'due_on' => '2031-04-05', 'amount_paise' => 118000, 'status' => 'paid'],
['batch' => 'Physics Evening', 'student' => 'Meera Iyer', 'due_on' => '2031-04-10', 'amount_paise' => 95000, 'status' => 'paid'],
['batch' => 'Physics Evening', 'student' => 'Rohan Das', 'due_on' => '2031-04-10', 'amount_paise' => 95000, 'status' => 'overdue'],
['batch' => 'Maths Morning', 'student' => 'Asha Verma', 'due_on' => '2031-05-05', 'amount_paise' => 118000, 'status' => 'paid'],
['batch' => 'Maths Morning', 'student' => 'Kabir Singh', 'due_on' => '2031-05-05', 'amount_paise' => 118000, 'status' => 'overdue'],
['batch' => 'Physics Evening', 'student' => 'Meera Iyer', 'due_on' => '2031-05-10', 'amount_paise' => 95000, 'status' => 'paid'],
['batch' => 'Physics Evening', 'student' => 'Rohan Das', 'due_on' => '2031-05-10', 'amount_paise' => 95000, 'status' => 'overdue'],
['batch' => 'Maths Morning', 'student' => 'Asha Verma', 'due_on' => '2031-06-05', 'amount_paise' => 118000, 'status' => 'pending'],
['batch' => 'Maths Morning', 'student' => 'Kabir Singh', 'due_on' => '2031-06-05', 'amount_paise' => 118000, 'status' => 'pending'],
['batch' => 'Physics Evening', 'student' => 'Meera Iyer', 'due_on' => '2031-06-10', 'amount_paise' => 95000, 'status' => 'pending'],
['batch' => 'Physics Evening', 'student' => 'Rohan Das', 'due_on' => '2031-06-10', 'amount_paise' => 95000, 'status' => 'pending'],
]; """A fee report from twelve instalment rows with one collection pipeline: filter, map, groupBy, sum, reduce and keyBy,
each step printed. Files: the folder report/ next to this file (instalments.php holds the rows)."""
from session import run
run(
"""
php report_probe.php
""",
app=True,
overlay=("report",),
) Output
$ php report_probe.php
rows: 12
paid rows: 5
collected, by batch and month (rupees):
Maths Morning 2031-04 2,360.00
Maths Morning 2031-05 1,180.00
Physics Evening 2031-04 950.00
Physics Evening 2031-05 950.00
collected per batch: {"Maths Morning":"3,540.00","Physics Evening":"1,900.00"}
overdue per student: {"Rohan Das":"1,900.00","Kabir Singh":"1,180.00"}
keyBy(student) on 12 rows keeps 4 rows, Asha's due_on: 2031-06-05
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 report_session.py
Step through what each method left in the collection:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Twelve rows in a collection. collect() wraps the array of 12 instalments of two batches, April to June, so every later step is a method call that returns a new collection.
- Keep the paid rows. filter() runs its closure on each row and keeps the rows for which it returns true. Five of the twelve were paid; the rest are overdue or pending.
- Add the month to each row. map() returns a new row for each old one. Here it adds a month key, 2031-04 or 2031-05, cut from the due date, and the original rows stay as they were.
- Group by batch, then month. groupBy() with two keys nests the groups, first by batch and then by month. Maths Morning has April and May, and Physics Evening has the same two months.
- Total each group. The outer map() visits each batch and the inner map() sums each month's group. The run printed 2,360.00 for Maths Morning in April, the two paid rows of 1,180.00.
Five of the twelve rows were paid. groupBy(['batch', 'month']) nested the groups by batch and month, and the inner sum gave 2,360.00 rupees for Maths Morning in April. reduce() folds a group into one value with a closure of your own and gave the same per-batch totals. Writing map->sum(...) is a higher-order message: A short form for a collection method that calls the same method on each item: map->sum('amount_paise') means map with a closure that calls sum('amount_paise') on every item., short for a closure that calls sum on each item.
Building a lookup with keyBy when a key repeats
keyBy('student') turned 12 rows into 4 and kept each student’s last row, Asha’s June instalment. Use groupBy() when a key can repeat, and keep keyBy() for keys that are unique.
For an input too large for memory, lazy collection: A collection that produces its items one at a time from a generator instead of holding an array, so its methods can work through an input larger than memory. offer the same methods but read one item at a time.
Checkpoint
Check: A collection pipeline for a report
Str and Number format strings and numbers for readers
Str holds Laravel’s helpers for strings, and Str::of() starts a fluent string: The object Str::of() returns: it holds a string and offers Laravel's string helpers as chainable methods, so trimming, changing case and cutting can be written one after another. whose methods chain like a collection’s. Number formats numbers through PHP’s intl extension, whose locale data decides the grouping:
"""Strings, numbers and dates for a fee reminder: Str and fluent strings, Number with the en and en_IN locales, and
Carbon storing UTC while showing Asia/Kolkata. Files: the folder formats/ next to this file."""
from session import run
run(
"""
php formats_probe.php
""",
app=True,
overlay=("formats",),
) Output
$ php formats_probe.php -- strings name as typed ' asha VERMA ' Str::of()->squish()->title() Asha Verma Str::slug(title) fee-reminder-april-2031-batch Str::limit(text, 24) Your April instalment fo... Str::mask(phone, *, 3, 5) +91*****00001 Str::headline(column name) Amount Paise -- numbers Number::format(en) 123,456.78 Number::format(en_IN) 1,23,456.78 Number::currency(INR, en_IN) ₹1,23,456.78 Number::currency(INR, en) ₹123,456.78 Number::percentage(0.875 * 100) 87.5% Number::forHumans(12345678) 12 million Number::abbreviate(12345678) 12M -- dates app timezone UTC stored (UTC) 2031-04-05 09:30:00 shown in Asia/Kolkata 2031-04-05 03:00 PM due date + 10 days 2031-04-15 days between 5 Apr and 15 May 40
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 formats_session.py
The run squished a name typed as ' asha VERMA ' into Asha Verma, masked five digits of a phone number and cut a reminder to 24 characters. The same amount printed 123,456.78 in the en locale and 1,23,456.78 in en_IN, and the currency kept that grouping. Number::forHumans gives the short international form, 12 million.
Indian digit grouping and short forms
In India, digits are grouped as thousands, then lakh (1,00,000) and crore (1,00,00,000), which is what the en_IN locale prints, with the rupee sign from Number::currency(..., in: 'INR', locale: 'en_IN'). Number::forHumans has no lakh or crore option, so the short forms Indian readers expect need a small helper of your own.
Carbon stores UTC and shows the reader’s time
A new Laravel 13 project runs in UTC (config('app.timezone') printed UTC), and Carbon, the library behind now() and every date cast, keeps dates in that zone. Store in UTC and convert only when you show a value:
| What the run did | Result |
|---|---|
| A payment stored in UTC | 2031-04-05 09:30:00 |
The same moment shown in Asia/Kolkata |
2031-04-05 03:00 PM |
| A due date plus 10 days | 2031-04-15 |
| Days between two due dates | 40 |
Carbon also does the calendar arithmetic, such as adding 10 days to a due date or counting the 40 days between two due dates in the run. India is 5 hours 30 minutes ahead of UTC, so dates near midnight change their day in India, and Carbon formats dates in Hindi as well. A daily report must cut its days at India’s midnight.
Checkpoint
Check: Str and Number with a locale · Store UTC and show local time
Lazy collections and cursors for large inputs, with memory and queries measured Pro: opens with a Pro, Premium or Ultimate pass
A lazy collection keeps only the item it is working on, so a report over a large export runs in a few megabytes. This part measures that, shows the trap of reading one twice, and compares the two ways to stream a database table.
In this part:
- A lazy collection pulls one item at a time from a generator
- Reading a lazy collection twice runs its source twice
- Streaming a table with cursor() or lazyById()
- Exporting a year of payments without holding it in memory
2 runnable examples (PHP, Python) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Short Indian amounts, Hindi slugs and dates, and the day boundaries of a UTC timestamp, recorded Pro: opens with a Pro, Premium or Ultimate pass
Laravel’s formatting helpers follow international defaults, and three of them surprise an app built for readers in India. This part records each surprise on the pinned toolchain and writes the fix with a test.
In this part:
- Short Indian amounts need a helper of your own
- Hindi titles lose their words in Str::slug()
- Store UTC, cut days at India’s midnight, and keep dates immutable
2 runnable examples (IndianAmount.php, IndianAmountTest.php, Python, india_probe.php) · about 7 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise
Build Pathshala’s monthly collection report in your own project:
- Put the 12 instalment rows of this lesson in a collection, and print the total paid per batch and month with
groupBy()andsum(). - Add a column that shows each total with
Number::currency(..., in: 'INR', locale: 'en_IN'). - Store one payment at
09:30UTC and print it inAsia/Kolkatawith Carbon.
The questions below check the first three sections:
Practice that adapts to you
Practise this lesson
7 questions about 8 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up: what is the difference between map() and each() on a Laravel collection?
map() returns a new collection with the result of the callback for every item, and leaves the original as it was. each() runs the callback for its side effect, returns the same collection, and stops early if the callback returns false. Use map() to transform values and each() to act on them.
These three take the lesson’s tools into harder cases:
-
Mid to senior
Pathshala's centres in India want a "collected today" figure on the dashboard and a daily total for the last 30 days. Payments are stored in UTC. A teammate groups by DATE(paid_at) in SQL. Explain what goes wrong, and design the query and the test.
-
Mid to senior
An accountant's "download all payments" button builds a CSV with Payment::all()->map(...) and fails with "Allowed memory size exhausted" once the table passes a few hundred thousand rows. Rewrite the export so its memory does not grow with the table, and say what you would measure.
-
Fresher to senior
A course fee of 10,000 rupees is split into 3 equal instalments, and the report adds the instalments of 1,200 students. A developer stores amounts as floats in rupees and rounds when printing. Explain what goes wrong and how you would store, split and total the money.
Flashcards · 7 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
How does a collection turn rows into a report?
collect($rows) wraps the array in an object whose chaining methods, such as groupBy() and map(), return a new collection and leave the original as it was, so the report reads top to bottom as its steps. Eloquent queries return collections too.
-
What did groupBy(['batch', 'month']) and reduce() do in the fee report?
groupBy nested the paid rows by batch and then by month, so an inner sum gave each batch's monthly total, 2,360.00 rupees for Maths Morning in April. reduce() folds a group into one value with your own closure.
-
What does map->sum(...) mean on a collection?
It is a higher-order message, short for a closure that calls sum on each item.
-
Why did keyBy('student') turn 12 rows into 4?
keyBy keeps one item per key, the last one, so each student kept only their last row. Use groupBy() when a key can repeat and keyBy() only for unique keys.
-
Which Str helpers did the lesson use to clean a name, a phone number and a reminder?
Str::of() starts a fluent string whose methods chain: squish() then title() turned a name typed with stray spaces and capitals into Asha Verma. Str::mask() hid five digits of a phone number and Str::limit() cut a reminder to 24 characters.
-
How does Number::format print 123456.78 in the en and en_IN locales?
123,456.78 in en and 1,23,456.78 in en_IN, Indian grouping by thousands, then lakh and crore, through PHP's intl extension. Number::currency(..., in: 'INR', locale: 'en_IN') keeps that grouping with the rupee sign; Number::forHumans has no lakh or crore form.
-
In which time zone does a new Laravel 13 project keep dates, and when do you convert?
UTC: config('app.timezone') prints UTC and Carbon keeps dates in it. Store in UTC and convert only to show a value; India is 5 hours 30 minutes ahead, so times near midnight change their date, and a daily report must cut its days at India's midnight.
Key takeaways
- Collections chain small steps into a report:
filter,map,groupByandsumproduced four batch-month totals from twelve rows, andkeyBykeeps only the last row of a repeated key. Strand fluent strings clean and shorten text, andNumberformats numbers with the locale’s grouping, soen_INprints 1,23,456.78.- Carbon keeps dates in UTC, the app’s time zone, and converts them only when a value is shown in India’s time.
Terms in this lesson
- collection
- A Laravel object that wraps an array and offers chainable methods such as filter, map, groupBy and sum, each returning a new collection. collect() makes one, and Eloquent queries return them.
- fluent string
- The object Str::of() returns: it holds a string and offers Laravel's string helpers as chainable methods, so trimming, changing case and cutting can be written one after another.
- generator
- A PHP function that uses yield to hand out values one at a time and pauses between them, instead of building and returning a whole array.
- higher-order message
- A short form for a collection method that calls the same method on each item: map->sum('amount_paise') means map with a closure that calls sum('amount_paise') on every item.
- lazy collection
- A collection that produces its items one at a time from a generator instead of holding an array, so its methods can work through an input larger than memory.
References
- Collections (Laravel (laravel.com))
- Strings (Laravel (laravel.com))
- Helpers (numbers and dates) (Laravel (laravel.com))
- Eloquent (chunking, lazy and cursor) (Laravel (laravel.com))
- Carbon documentation (Carbon (briannesbitt/Carbon))
- Generators overview (The PHP Group (php.net))
- The NumberFormatter class (intl) (The PHP Group (php.net))
- Unicode character properties in regular expressions (The PHP Group (php.net))
- Time zone database (IANA)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress