NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Advertisements for Laravel

Creating & updating ads

Both create and update take an AdvertisementData. Only name is required; price is a nullable RoundlyConsulting\Money\Money (a price-less ad passes price: null or omits it). On create an omitted argument is simply empty; on update it means “keep the stored value”:

use Illuminate\Support\Collection;
use RoundlyConsulting\Advertisements\DataTransferObjects\AdvertisementData;
use RoundlyConsulting\Advertisements\Facades\Advertisements;
use RoundlyConsulting\Money\Money;

$ad = Advertisements::create(new AdvertisementData(
    name: 'Vintage road bike',
    price: Money::ofMinor(25000, 'EUR'),  // 250.00 EUR
    category: $bikesCategory,             // a Category model, an id, or a slug — resolved to category_id
    description: 'Lightly used, great condition.',
    author: $user,                        // any Eloquent model (polymorphic author)
    meta: new Collection(['featured' => true]),
    publishedAt: now(),
    expiresAt: now()->addMonth(),
));                                       // → AdvertisementCreated
ArgumentTypeStored as
namestringRequired. Stored under the active locale and slugged per locale.
price?MoneyAmount in price (minor units), code in currency. Omit for a price-less ad.
categoryCategory|int|string|nullModel, id or slug, resolved to category_id; an unknown slug resolves to null.
description?stringStored under the active locale.
author?ModelAny Eloquent model — the polymorphic author.
meta?CollectionArbitrary extra details, stored as JSON.
publishedAt?CarbonInterfacepublished_at.
expiresAt?CarbonInterfaceexpires_at.

create() fires AdvertisementCreated. Passing publishedAt makes the ad live (or scheduled) from that instant — the status accessor and the scopes read the dates — but only publish() fires AdvertisementPublished, so call it when listeners depend on that event.

Exact price factories

Or use a factory that names the unit, defaulting the currency to default_currency. Neither accepts a float:

// Minor units (cents): an int or an integer string — wider than int64 is fine.
$data = AdvertisementData::fromMinor(name: 'Vintage road bike', minor: 25000);          // 250.00 EUR
$data = AdvertisementData::fromMinor(name: 'Bike', minor: '00199', currency: 'USD');    // 1.99 USD

// Decimal major units: exact, exponent-aware, never rounded.
$data = AdvertisementData::fromDecimal(name: 'Bike', amount: '1.10');                   // 110 cents
$data = AdvertisementData::fromDecimal(name: 'Bike', amount: '1500', currency: 'JPY');  // 1500 yen
AdvertisementData::fromDecimal(name: 'Bike', amount: '19.999');                         // throws RoundingNecessary

// A price-less ad: the constructor with no price
$data = new AdvertisementData(name: 'Free to a good home');

Both take the same optional category, description, author, meta, publishedAt and expiresAt arguments as the constructor.

From a request

Validate with money’s rules and read the price with Request::money() — it converts a JSON float (1.1) exactly, where (int) ceil($request->float('price') * 100) stores 1.10 as 1.11:

use RoundlyConsulting\Advertisements\DataTransferObjects\AdvertisementData;
use RoundlyConsulting\Money\Rules\{CurrencyCode, MoneyAmount};

$request->validate([
    'price'    => ['required', MoneyAmount::inCurrencyFrom('currency')->min('0')],
    'currency' => ['required', new CurrencyCode],
]);

// Preferred — JSON or form bodies:
$data = new AdvertisementData(
    name: (string) $request->string('name'),
    price: $request->money('price', currencyKey: 'currency'),
);

// Form-encoded input only (the value is already a string):
$data = AdvertisementData::fromDecimal(
    name: (string) $request->string('name'),
    amount: (string) $request->string('price'),
    currency: (string) $request->string('currency'),
);

Updating

$ad = Advertisements::update($ad, new AdvertisementData(
    name: 'Vintage road bike (reduced)',
    price: Money::ofMinor(19900, 'EUR'),
));                                       // → AdvertisementUpdated

update() changes only the fields you pass. name is always required; every argument you leave out keeps the ad’s stored value, so the price cut above leaves the description, category, meta, author, placements and the publishedAt / expiresAt dates alone — and a live ad stays live.

  • Pass an argument as an explicit null to clear it: author: null dissociates the author, category: null uncategorises the ad, publishedAt: null takes it back to draft.
  • fromMinor() and fromDecimal() follow the same rule for their optional arguments.
  • name and description are written under the active locale; other locales keep their values.
  • A price in another currency re-denominates the ad (the action writes the currency column first); price: null clears the amount and leaves currency as it was.
  • AdvertisementData::provides($field) tells whether an optional field was passed — an explicit null counts.
// Pass an argument as an explicit null to clear it
$ad = Advertisements::update($ad, new AdvertisementData(
    name: $ad->name,
    author: null,        // dissociates the author
    category: null,      // uncategorises the ad
    publishedAt: null,   // back to draft
));

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.