PHPackages                             aftandilmmd/laravel-poller - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. aftandilmmd/laravel-poller

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

aftandilmmd/laravel-poller
==========================

A powerful, flexible poll and voting package for Laravel. Supports multiple poll types, anonymous voting, scheduled polls, and Livewire components.

v1.0.0(3mo ago)10MITPHPPHP ^8.2

Since Feb 14Pushed 1mo agoCompare

[ Source](https://github.com/aftandilmmd/laravel-poller)[ Packagist](https://packagist.org/packages/aftandilmmd/laravel-poller)[ RSS](/packages/aftandilmmd-laravel-poller/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (4)Versions (2)Used By (0)

**English** | [Türkçe](README.tr.md) | [Azərbaycanca](README.az.md)

Laravel Poller
==============

[](#laravel-poller)

A powerful, flexible poll and voting package for Laravel. Supports 5 poll types, anonymous voting, scheduled polls, vote changing, and both Livewire components and a RESTful API.

Requirements
------------

[](#requirements)

- PHP 8.2+
- Laravel 11, 12, or 13 (Laravel 13 requires PHP 8.3+)

Installation
------------

[](#installation)

```
composer require aftandilmmd/laravel-poller
```

The service provider and facade are auto-discovered.

Publish the config file:

```
php artisan vendor:publish --tag=poller-config
```

Publish migrations (optional - migrations run automatically):

```
php artisan vendor:publish --tag=poller-migrations
```

Publish views (optional - for customization):

```
php artisan vendor:publish --tag=poller-views
```

Publish translations (optional - for customization):

```
php artisan vendor:publish --tag=poller-translations
```

Run migrations:

```
php artisan migrate
```

Configuration
-------------

[](#configuration)

Full config options in `config/poller.php`:

KeyDescriptionDefault`user_model`Your User model class`App\Models\User``tables.polls`Polls table name`poller_polls``tables.options`Options table name`poller_poll_options``tables.votes`Votes table name`poller_poll_votes``features.anonymous_voting`Enable anonymous voting`true``features.vote_changing`Enable vote changing`true``features.vote_retraction`Enable vote retraction`true``features.vote_comments`Enable vote comments`true``features.auto_close`Auto-close expired polls`true``features.auto_open`Auto-open scheduled polls`true``features.custom_options`Allow users to add custom options`true``features.poll_scheduling`Enable poll scheduling (starts\_at/ends\_at)`true``features.soft_deletes`Enable soft deletes on polls`true``rating.min`Rating scale minimum`1``rating.max`Rating scale maximum`5``pagination.polls`Polls per page`20``pagination.votes`Votes per page`50``api.enabled`Enable REST API routes`false``api.rate_limit`API requests per minute`60`---

Setup
-----

[](#setup)

### Add poll support to any model (Pollable)

[](#add-poll-support-to-any-model-pollable)

```
use Aftandilmmd\Poller\Traits\HasPolls;

class Meeting extends Model
{
    use HasPolls;
}
```

### Add voting capabilities to User model

[](#add-voting-capabilities-to-user-model)

```
use Aftandilmmd\Poller\Traits\InteractsWithPolls;

class User extends Authenticatable
{
    use InteractsWithPolls;

    // Override for custom authorization:
    public function canCreatePoll(): bool
    {
        return $this->is_admin;
    }

    public function canVote(Poll $poll): bool
    {
        return $poll->isVotingOpen() && $this->hasActiveSubscription();
    }

    public function canAddCustomOption(Poll $poll): bool
    {
        return $poll->allowsCustomOptions() && $this->is_premium;
    }

    public function canManagePoll(Poll $poll): bool
    {
        return $poll->created_by === $this->id || $this->is_admin;
    }
}
```

---

Poll Types
----------

[](#poll-types)

TypeDescription`YesNo`Simple yes/no voting`SingleChoice`Select one option`MultipleChoice`Select multiple options (with min/max constraints)`Rating`Rate options on a configurable scale (default 1-5)`Ranked`Rank options by preference---

Usage
-----

[](#usage)

### Via Facade

[](#via-facade)

```
use Aftandilmmd\Poller\Facades\Poller;

// Create a poll
$poll = Poller::create([
    'title' => 'Best framework?',
    'type' => 'single_choice',
    'is_anonymous' => false,
    'show_results_before_close' => true,
    'allow_vote_change' => true,
], $user);

// Add options
Poller::addOption($poll, ['title' => 'Laravel']);
Poller::addOption($poll, ['title' => 'Django']);
Poller::addOption($poll, ['title' => 'Rails']);

// Activate the poll
Poller::activate($poll);

// Cast a vote
Poller::castVote($poll, $user, $optionId);

// Cast vote with comment
Poller::castVote($poll, $user, $optionId, ['comment' => 'Great choice!']);

// Change a vote
Poller::changeVote($poll, $user, $newOptionId);

// Retract a vote
Poller::retractVote($poll, $user);

// Get results
$results = Poller::getResults($poll);
// [['option_id' => 1, 'title' => 'Laravel', 'votes_count' => 15, 'percentage' => 75.0], ...]

$detailed = Poller::getDetailedResults($poll);
// ['poll' => ..., 'total_votes' => 20, 'unique_voters' => 18, 'options' => [...], 'leading_option' => ...]

// Lifecycle
Poller::close($poll);
Poller::cancel($poll);

// Reorder options
Poller::reorderOptions($poll, [$optionId3, $optionId1, $optionId2]);

// Duplicate a poll (copies all options)
$newPoll = Poller::duplicate($poll, ['title' => 'Copy of poll']);
```

### Custom Options

[](#custom-options)

Allow voters to add their own options to a poll. Control the maximum number and who can add them.

```
// Create a poll with custom options enabled (max 5)
$poll = Poller::create([
    'title' => 'Best framework?',
    'type' => 'single_choice',
    'allow_custom_options' => true,
    'max_custom_options' => 5, // null = unlimited
], $user);

// Add a custom option (via Facade)
Poller::addCustomOption($poll, $user, ['title' => 'My suggestion']);

// Add a custom option (via User model)
$user->addCustomOption($poll, ['title' => 'My suggestion']);

// Check helpers
$poll->allowsCustomOptions();         // true
$poll->getCustomOptionCount();        // 1
$poll->hasReachedCustomOptionLimit(); // false
$option->isCustom();                  // true
$option->creator;                     // User who added it
```

Override `canAddCustomOption()` in your User model to control authorization:

```
public function canAddCustomOption(Poll $poll): bool
{
    return $poll->allowsCustomOptions() && $this->is_premium;
}
```

The Livewire `PollVote (poller-poll-vote)` widget automatically shows a "Add your own option" input when custom options are enabled and the user is authorized.

### Via Poll Model

[](#via-poll-model)

```
// Lifecycle
$poll->activate();
$poll->close();
$poll->cancel();

// Reorder options
$poll->reorderOptions([$optionId3, $optionId1, $optionId2]);

// Duplicate
$newPoll = $poll->duplicate(['title' => 'Copy']);
```

### Via Pollable Model

[](#via-pollable-model)

```
// Create a poll attached to a meeting
$poll = $meeting->createPoll([
    'title' => 'Meeting agenda vote',
    'type' => 'multiple_choice',
    'min_selections' => 1,
    'max_selections' => 3,
], $user);

// Get polls
$meeting->polls;
$meeting->activePolls;
$meeting->closedPolls;
$meeting->hasPollsInProgress();
```

### Via User Model (InteractsWithPolls trait)

[](#via-user-model-interactswithpolls-trait)

```
$user->vote($poll, $optionId);
$user->changeVote($poll, $newOptionId);
$user->retractVote($poll);
$user->hasVotedOn($poll);     // true/false
$user->getVotesFor($poll);    // Collection of PollVote
$user->createdPolls;           // HasMany
$user->pollVotes;              // HasMany
```

---

Livewire Components
-------------------

[](#livewire-components)

The package includes 5 ready-to-use Livewire components with full Tailwind CSS UI (dark mode supported).

> **Note:** Livewire components are optional. Projects without Livewire can use the Facade API or REST API directly.

### Poll Manager (Full CRUD)

[](#poll-manager-full-crud)

```

{{-- Scoped to a specific model --}}

```

Features: Search, filter by status/type, create, edit, delete, activate, close, duplicate polls.

### Poll Form (Create/Edit)

[](#poll-form-createedit)

```

```

### Poll Display (Full View)

[](#poll-display-full-view)

```

```

Shows poll info, stats, voting UI, results, and vote history tabs.

### Poll Results (Analytics)

[](#poll-results-analytics)

```

```

Displays bar chart results with percentages and leading option.

### Vote Widget (Compact)

[](#vote-widget-compact)

```

```

Embeddable voting widget. Handles all 5 poll types with the appropriate UI (radio, checkbox, rating scale, ranking).

### Customizing Views

[](#customizing-views)

```
php artisan vendor:publish --tag=poller-views
```

Views will be published to `resources/views/vendor/poller/`.

---

REST API
--------

[](#rest-api)

Enable the API in your config:

```
// config/poller.php
'api' => [
    'enabled' => true,
    'prefix' => 'api/polls',
    'middleware' => ['api', 'auth:sanctum'],
    'rate_limit' => 60, // requests per minute (null to disable)
],
```

All mutation endpoints (update, delete, lifecycle actions, option management) enforce ownership checks. If your User model uses the `InteractsWithPolls` trait, the `canManagePoll()` method is used for authorization.

API responses use Eloquent API Resources for consistent JSON formatting.

### Endpoints

[](#endpoints)

MethodEndpointDescription`GET``/api/polls`List polls (with filters)`POST``/api/polls`Create poll`GET``/api/polls/{poll}`Show poll`PUT``/api/polls/{poll}`Update poll`DELETE``/api/polls/{poll}`Delete poll`POST``/api/polls/{poll}/activate`Activate`POST``/api/polls/{poll}/close`Close`POST``/api/polls/{poll}/cancel`Cancel`POST``/api/polls/{poll}/duplicate`Duplicate`POST``/api/polls/{poll}/options`Add option`PUT``/api/polls/{poll}/options/{option}`Update option`DELETE``/api/polls/{poll}/options/{option}`Remove option`POST``/api/polls/{poll}/options/reorder`Reorder options`POST``/api/polls/{poll}/vote`Cast vote`PUT``/api/polls/{poll}/vote`Change vote`DELETE``/api/polls/{poll}/vote`Retract vote`GET``/api/polls/{poll}/results`Get results`GET``/api/polls/{poll}/votes`List votes### Example: Cast a Vote

[](#example-cast-a-vote)

```
curl -X POST /api/polls/1/vote \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"options": [3], "comment": "My pick"}'
```

---

Commands
--------

[](#commands)

### Scheduled Commands

[](#scheduled-commands)

Add to your scheduler for automatic poll lifecycle management:

```
// routes/console.php or bootstrap/app.php
Schedule::command('poller:auto-open')->everyMinute();
Schedule::command('poller:auto-close')->everyMinute();
```

- `poller:auto-open` -- Activates draft polls whose `starts_at` has passed
- `poller:auto-close` -- Closes active polls whose `ends_at` has passed

### Maintenance Commands

[](#maintenance-commands)

```
# Recalculate all option vote counts from actual vote records
php artisan poller:reconcile-counts
```

---

Events
------

[](#events)

All events are configurable via `config/poller.php`. Set to `null` to disable.

EventPayload`PollCreated`Poll, creator`PollActivated`Poll`PollClosed`Poll`PollCancelled`Poll`VoteCast`Poll, voter, votes`VoteChanged`Poll, voter, oldVotes, newVotes`VoteRetracted`Poll, voter```
// Listen to events
Event::listen(VoteCast::class, function ($event) {
    // $event->poll, $event->voter, $event->votes
});
```

---

Advanced Features
-----------------

[](#advanced-features)

All advanced features below are **opt-in** via `config/poller.php`. Defaults keep behavior unchanged.

### Result Caching

[](#result-caching)

Cache poll results to avoid recomputing on every request. Cache is invalidated automatically when votes are cast, changed, or retracted.

```
// config/poller.php
'cache' => [
    'enabled' => true,
    'store' => null,         // null = default cache store
    'ttl' => 60,             // seconds
    'prefix' => 'poller',
],
```

```
$poll->getResultsAsPercentages();   // first call hits DB, subsequent calls hit cache
$poll->flushResultsCache();         // manual invalidation
```

### Broadcasting

[](#broadcasting)

Make poll/vote events broadcast over Laravel Echo / WebSockets. Channel name pattern: `{prefix}.{pollId}`.

```
// config/poller.php
'broadcasting' => [
    'enabled' => true,
    'channel' => 'private',          // private | presence | public
    'channel_prefix' => 'poller.poll',
],
```

```
// resources/js — listen on the frontend
Echo.private(`poller.poll.${pollId}`)
    .listen('VoteCast', (e) => updateChart(e.poll));
```

### Voter Rate Limiting

[](#voter-rate-limiting)

Limit how many votes a single voter can cast across all polls in a sliding window. Throws `VoterRateLimitException` when exceeded.

```
// config/poller.php
'voter_rate_limit' => [
    'enabled' => true,
    'max_votes' => 30,
    'per_minutes' => 60,
],
```

### Translatable Content

[](#translatable-content)

Store poll/option `title` and `description` as JSON locale maps. Returns the value for `app()->getLocale()` automatically, falls back to `fallback_locale`.

```
// config/poller.php
'translatable' => [
    'enabled' => true,
    'fallback_locale' => 'en',
],
```

```
// Create with translations
Poller::create([
    'title' => ['en' => 'Best framework?', 'tr' => 'En iyi framework?', 'az' => 'Ən yaxşı framework?'],
], $user);

// Read in current locale
app()->setLocale('tr');
$poll->title;                          // "En iyi framework?"

// Translation helpers
$poll->translate('title', 'az');       // "Ən yaxşı framework?"
$poll->setTranslation('title', 'tr', 'Yeni başlık')->save();
$poll->getTranslations('title');       // ['en' => '...', 'tr' => '...', 'az' => '...']
```

### Query Scopes

[](#query-scopes)

Search and filter polls with chainable scopes:

```
use Aftandilmmd\Poller\Models\Poll;
use Aftandilmmd\Poller\Enums\PollStatus;
use Aftandilmmd\Poller\Enums\PollType;

Poll::query()
    ->search('framework')                       // matches title or description
    ->ofStatus(PollStatus::Active)              // enum or string
    ->ofType(PollType::SingleChoice)
    ->createdBy($user->id)
    ->withinDateRange(now()->subMonth(), now())
    ->get();
```

---

Error Handling
--------------

[](#error-handling)

All voting errors throw typed exceptions:

```
use Aftandilmmd\Poller\Exceptions\PollClosedException;
use Aftandilmmd\Poller\Exceptions\AlreadyVotedException;
use Aftandilmmd\Poller\Exceptions\InvalidSelectionException;
use Aftandilmmd\Poller\Exceptions\UnauthorizedVoteException;
use Aftandilmmd\Poller\Exceptions\CustomOptionException;

try {
    Poller::castVote($poll, $user, $optionId);
} catch (PollClosedException $e) {
    // Poll is not accepting votes
} catch (AlreadyVotedException $e) {
    // User already voted (and vote_change is disabled)
} catch (InvalidSelectionException $e) {
    // Wrong number of selections or invalid option
} catch (UnauthorizedVoteException $e) {
    // User's canVote() returned false
} catch (CustomOptionException $e) {
    // Custom options not allowed, limit reached, or unauthorized
}
```

---

Enums
-----

[](#enums)

```
use Aftandilmmd\Poller\Enums\PollType;
use Aftandilmmd\Poller\Enums\PollStatus;

PollType::SingleChoice->value;   // "single_choice"
PollType::SingleChoice->label(); // "Single Choice"
PollType::SingleChoice->color(); // "green"
PollType::options();             // ["yes_no" => "Yes/No", ...]
PollType::enabled();             // Only config-enabled types

PollStatus::Active->value;      // "active"
PollStatus::Active->label();    // "Active"
PollStatus::Active->color();    // "green"
```

---

Extending
---------

[](#extending)

### Custom Models

[](#custom-models)

Override model classes in config:

```
'models' => [
    'poll' => App\Models\CustomPoll::class,
    'option' => App\Models\CustomPollOption::class,
    'vote' => App\Models\CustomPoller::class,
],
```

### Custom Events

[](#custom-events)

Replace event classes or disable them:

```
'events' => [
    'vote_cast' => App\Events\CustomVoteCast::class,
    'poll_created' => null, // Disabled
],
```

---

Translations
------------

[](#translations)

The package includes translations for English, Turkish, and Azerbaijani. To customize or add new languages:

```
php artisan vendor:publish --tag=poller-translations
```

Translation files will be published to `lang/vendor/poller/`.

---

Testing
-------

[](#testing)

```
composer install
vendor/bin/pest
```

---

Roadmap
-------

[](#roadmap)

### Shipped

[](#shipped)

- Core CRUD with 5 poll types (yes/no, single, multiple, rating, ranked)
- Anonymous voting, vote changing, vote retraction
- Scheduled polls with auto-open / auto-close commands
- User-suggested custom options with limits
- Vote comments and required-comment polls
- Result percentages, leading option, detailed results export
- REST API (18 endpoints)
- Livewire components (Manager, Form, Display, Vote, Results)
- Trait-based authorization (`InteractsWithPolls`, `HasPolls`)
- 7 lifecycle/voting events with broadcasting support
- Pollable morph (attach polls to any model)
- Soft deletes
- Result caching with auto-invalidation
- Voter rate limiting (cross-poll sliding window)
- Translatable title/description (opt-in JSON locale map)
- Query scopes: `search`, `ofStatus`, `ofType`, `createdBy`, `withinDateRange`
- API filter parameters (`search`, `status`, `type`, `created_by`, `from`, `to`)
- API returns `429` on voter rate limit
- Localized exception messages (en, tr, az)
- Laravel 11, 12, 13 support

### Considered for future

[](#considered-for-future)

- Translatable form fields in Livewire `PollForm` (multi-locale inputs)
- `PollResource@withTranslations` for API multi-locale output
- CSV / JSON export beyond `array`
- IP-based vote tracking (anonymous spam protection)
- Built-in tags / categories
- First-party Filament / Nova plugin

### Out of scope

[](#out-of-scope)

These belong in user code or sibling packages, not this one:

- Notifications (mail / database / broadcast on poll events) — wire your own listeners
- Captcha / spam middleware — apply at the route level
- Webhooks — listen to events and POST yourself
- Charts / analytics dashboard — render from `getDetailedResults()` data
- Audit log — use [`spatie/laravel-activitylog`](https://github.com/spatie/laravel-activitylog) on the events
- Short URLs / QR codes — use a dedicated package

---

License
-------

[](#license)

MIT

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance87

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

115d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/630ea45c41a7b5c54294a46920d0036cf0693865c7d3254c445b6cc08670edd0?d=identicon)[aftandilmmd](/maintainers/aftandilmmd)

---

Top Contributors

[![aftandilmmd](https://avatars.githubusercontent.com/u/26919900?v=4)](https://github.com/aftandilmmd "aftandilmmd (31 commits)")

---

Tags

laravellivewirepollvotingsurvey

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/aftandilmmd-laravel-poller/health.svg)

```
[![Health](https://phpackages.com/badges/aftandilmmd-laravel-poller/health.svg)](https://phpackages.com/packages/aftandilmmd-laravel-poller)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3325.1M337](/packages/psalm-plugin-laravel)[wearepixel/laravel-cart

A cart implementation for Laravel

1355.6k](/packages/wearepixel-laravel-cart)[tomshaw/electricgrid

A feature-rich Livewire package designed for projects that require dynamic, interactive data tables.

119.2k](/packages/tomshaw-electricgrid)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
