nunomaduro/laravel-meetups
A small Laravel package for listing and displaying community meetups in your app. It offers a straightforward way to showcase local events with minimal setup, making it easy to add a meetups directory or events section to your project.
Verify the shape first. Despite the "package" framing, the repo describes itself as a console application. Before running composer require, look for:
composer.json → "autoload.psr-4"? "extra.laravel.providers"?
src/ → ServiceProvider / Facade / config file?
README.md → `composer require` or `git clone && php artisan meetups`?
No service provider + no extra.laravel → treat it as a reference implementation, not a dependency. Copy the commands into app/Console/Commands rather than installing.
Install (only if it really is a package):
composer require nunomaduro/laravel-meetups
Expect resolution issues on modern Laravel. Fall back to the branch if needed:
composer require nunomaduro/laravel-meetups:dev-master --with-all-dependencies
Where to look first, in order:
README.md — the sole doc; the v1.2.0 release is literally "Updated readme".config/ — data source, cache TTL, city/group slug.src/Console/ — the Artisan command is the primary entry point per the project description.First use case: publish config, set a source (group slug or city), run the command to confirm data flows before touching views.
php artisan vendor:publish --provider="NunoMaduro\LaravelMeetups\MeetupsServiceProvider" --tag=config
php artisan list | grep -i meetup # discover the actual command signature
php artisan meetups # or meetups:list / meetups:near
Don't call package classes from controllers or Blade. One thin wrapper means one file to change when (not if) the upstream breaks:
// app/Services/MeetupDirectory.php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
class MeetupDirectory
{
public function upcoming(int $limit = 5): array
{
return Cache::remember('meetups.nearby', now()->addHours(6), function () use ($limit) {
// delegate to the package command's logic, or your own Http call
return $this->fetchFromSource($limit);
});
}
protected function fetchFromSource(int $limit): array
{
// your Http::get(...)->json() or the package class
}
}
Given the console-app framing, keep the acquisition in a scheduled command and let the web layer just read cache:
// app/Console/Commands/SyncMeetups.php
class SyncMeetups extends Command
{
protected $signature = 'meetups:sync';
protected $description = 'Refresh cached nearby meetups';
public function handle(MeetupDirectory $directory): int
{
$fresh = $directory->refresh(); // cache-busting fetch
$this->info(sprintf('Cached %d meetups', count($fresh)));
return $fresh === [] ? self::FAILURE : self::SUCCESS; // signal emptiness to the scheduler
}
}
// routes/console.php
Schedule::command('meetups:sync')->hourly()->withoutOverlapping();
Because the data source is unmaintained, put it behind config so a dark feature doesn't take up a slot in the page layout:
if (config('meetups.enabled')) {
return view('partials.meetups', ['meetups' => app(MeetupDirectory::class)->upcoming()]);
}
Best day-to-day use is a prototype scaffold: ship the Blade listing with static/hardcoded data to validate the UI, then swap in a live fetch only once the page proves out. This mirrors the original framing — sourcing venues, speakers, and sponsorship targets — which is an operational win for a DevRel workflow even if the product feature never lands.
The description overstates the package. "Fluent API", "Blade-ready output", "configurable caching" — none of that is verifiable from a single "updated readme" release. Treat every capability claim as aspirational until you see the source. The actual shape is an Artisan command, so plan your integration around that.
Framework drift will bite immediately. 2017 release → targets Laravel 5.4/5.5 and PHP 7.0/7.1. On Laravel 10+ / PHP 8.2+, expect --ignore-platform-reqs, removed str_*/array_* helpers, and Guzzle v6 idioms to cause friction. If the goal is a working feature, rewriting ~150 lines against Laravel's Http facade is usually faster than debugging the original.
The meetup data source is the real risk, not the PHP. Meetup.com's API moved to OAuth-gated paid access years ago. If the command scrapes, it's fragile by definition — gate it behind a feature flag and treat parse failures as first-class errors, not silent null.
Silent failures are the default. A scraper returning HTML instead of JSON, or an API returning [] on quota exhaustion, will exit 0 and quietly go dark. Return self::FAILURE on empty results and add a freshness check (assert cache age in the view or a status endpoint).
Don't install it as a Composer dependency you rely on. 10 stars, 0 dependents, last release June 2017, no security patching. If you must vendor it, pin to a commit and accept full ownership — but the honest option is copy-and-modernize into your own namespace.
Replace Guzzle with Http on day one. Beyond framework alignment, Http::fake() gives you the test coverage the original never had:
Http::fake(['*' => Http::response([['name' => 'Laravel Berlin', 'date' => '2024-06-01']])]);
$this->artisan('meetups:sync')->assertSuccessful();
Cache aggressively and stagger. Event cadence is hours, not minutes. Cache::remember with a 4–6 hour TTL and ->hourly()->withoutOverlapping() protects the upstream quota — which, not compute, is your real ceiling if you fan out across cities.
Watch timezones and "near you". If geolocation is resolved from a stale default or a config slug, the "nearby" framing silently degrades. Pin an explicit timezone in config and render dates with ->timezone(config('app.timezone')).
Extension points are thin, by design. There's no driver/repository abstraction despite what the summary implies. Your realistic extension surface is: fork the command, swap the fetch, add caching and formatting. That's a few hours of reading — budget accordingly and don't go looking for a plugin architecture that isn't there.
How can I help you explore Laravel packages today?