├── .github └── workflows │ └── tests.yml ├── .whitesource ├── LICENSE.md ├── composer.json ├── config └── survey.php ├── database ├── factories │ ├── AnswerFactory.php │ ├── QuestionFactory.php │ └── SurveyFactory.php └── migrations │ ├── create_answers_table.php.stub │ ├── create_entries_table.php.stub │ ├── create_questions_table.php.stub │ ├── create_sections_table.php.stub │ └── create_surveys_table.php.stub ├── readme.md ├── resources └── views │ ├── partials │ └── question.blade.php │ ├── questions │ ├── base.blade.php │ ├── single.blade.php │ └── types │ │ ├── multiselect.blade.php │ │ ├── number.blade.php │ │ ├── radio.blade.php │ │ └── text.blade.php │ ├── sections │ └── single.blade.php │ └── standard.blade.php └── src ├── Casts └── SeparatedByCommaAndSpace.php ├── Contracts ├── Answer.php ├── Entry.php ├── Question.php ├── Section.php ├── Survey.php └── Value.php ├── Exceptions ├── GuestEntriesNotAllowedException.php └── MaxEntriesPerUserLimitExceeded.php ├── Http └── View │ └── Composers │ └── SurveyComposer.php ├── Models ├── Answer.php ├── Entry.php ├── Question.php ├── Section.php └── Survey.php ├── SurveyServiceProvider.php └── Utilities └── Summary.php /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | test: 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | php: [8.1, 8.2, 8.3, 8.4] 21 | laravel: ['10.*', '11.*', '12.*'] 22 | dependency-version: [prefer-stable] 23 | exclude: 24 | - laravel: 11.* 25 | php: 8.1 26 | - laravel: 12.* 27 | php: 8.1 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | 32 | - name: Setup PHP 33 | uses: shivammathur/setup-php@v2 34 | with: 35 | php-version: ${{ matrix.php }} 36 | 37 | - name: Install dependencies 38 | run: | 39 | composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update 40 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction 41 | 42 | - name: Run tests 43 | run: vendor/bin/phpunit 44 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "checkRunSettings": { 3 | "vulnerableCheckRunConclusionLevel": "failure" 4 | }, 5 | "issueSettings": { 6 | "minSeverityLevel": "LOW" 7 | } 8 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Matt Daneshvar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "matt-daneshvar/laravel-survey", 3 | "description": "Create surveys inside your Laravel app", 4 | "keywords": [ 5 | "laravel", 6 | "survey" 7 | ], 8 | "type": "library", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Matt Daneshvar", 13 | "email": "matt.daneshvar@gmail.com" 14 | } 15 | ], 16 | "require": { 17 | "php": "^8.1", 18 | "illuminate/database": "^10.0|^11.0|^12.0" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "^10.0|^11.0", 22 | "orchestra/testbench": "^8.0|^9.0|^10.0", 23 | "illuminate/http": "^10.0|^11.0|^12.0", 24 | "laravel/legacy-factories": "^1.4" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "MattDaneshvar\\Survey\\": "src/" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "MattDaneshvar\\Survey\\Tests\\": "tests/" 34 | }, 35 | "files": [ 36 | "tests/utilities/helpers.php" 37 | ] 38 | }, 39 | "extra": { 40 | "laravel": { 41 | "providers": [ 42 | "MattDaneshvar\\Survey\\SurveyServiceProvider" 43 | ] 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /config/survey.php: -------------------------------------------------------------------------------- 1 | [ 9 | /* 10 | * Name of the tables created by the migrations 11 | * and used by the models of this package. 12 | */ 13 | 'tables' => [ 14 | 'surveys' => 'surveys', 15 | 'sections' => 'sections', 16 | 'questions' => 'questions', 17 | 'entries' => 'entries', 18 | 'answers' => 'answers', 19 | ], 20 | ], 21 | ]; 22 | -------------------------------------------------------------------------------- /database/factories/AnswerFactory.php: -------------------------------------------------------------------------------- 1 | define(Answer::class, function (Faker $faker) { 8 | return [ 9 | 'value' => $faker->words(3, true), 10 | 'question_id' => factory(Question::class)->create()->id, 11 | ]; 12 | }); 13 | -------------------------------------------------------------------------------- /database/factories/QuestionFactory.php: -------------------------------------------------------------------------------- 1 | define(Question::class, function (Faker $faker) { 7 | return [ 8 | 'content' => $faker->name, 9 | ]; 10 | }); 11 | -------------------------------------------------------------------------------- /database/factories/SurveyFactory.php: -------------------------------------------------------------------------------- 1 | define(Survey::class, function (Faker $faker) { 7 | return [ 8 | 'name' => $faker->name, 9 | ]; 10 | }); 11 | -------------------------------------------------------------------------------- /database/migrations/create_answers_table.php.stub: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->unsignedInteger('question_id'); 19 | $table->unsignedInteger('entry_id')->nullable(); 20 | $table->text('value'); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists(config('survey.database.tables.answers')); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/create_entries_table.php.stub: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->unsignedInteger('survey_id'); 19 | $table->unsignedInteger('participant_id')->nullable(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::dropIfExists(config('survey.database.tables.entries')); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/migrations/create_questions_table.php.stub: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->unsignedInteger('survey_id')->nullable(); 19 | $table->unsignedInteger('section_id')->nullable(); 20 | $table->text('content'); 21 | $table->string('type')->default('text'); 22 | $table->json('options')->nullable(); 23 | $table->json('rules')->nullable(); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists(config('survey.database.tables.questions')); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/create_sections_table.php.stub: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->unsignedInteger('survey_id')->nullable(); 19 | $table->string('name'); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::dropIfExists(config('survey.database.tables.sections')); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/migrations/create_surveys_table.php.stub: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('name'); 19 | $table->json('settings')->nullable(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::dropIfExists(config('survey.database.tables.surveys')); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Laravel Survey 2 | [![Tests](https://github.com/matt-daneshvar/laravel-survey/actions/workflows/tests.yml/badge.svg)](https://github.com/matt-daneshvar/laravel-survey/actions/workflows/tests.yml) 3 | ![GitHub](https://img.shields.io/github/license/matt-daneshvar/laravel-survey) 4 | 5 | Create and manage surveys within your Laravel app. 6 | 7 | ![Demo](https://github.com/matt-daneshvar/laravel-survey/assets/10030505/1fd79b4b-5058-4049-a369-8439b0431fe2) 8 | 9 | [This video](https://youtu.be/BA7tc-2rcWg) walks through installing this package and creating a basic survey. 10 | 11 | ## Installation 12 | Require the package using composer. 13 | ```bash 14 | composer require matt-daneshvar/laravel-survey 15 | ``` 16 | 17 | Publish the package migrations. 18 | ```bash 19 | php artisan vendor:publish --provider="MattDaneshvar\Survey\SurveyServiceProvider" --tag="migrations" 20 | ``` 21 | 22 | Run the migrations to create all the required tables. 23 | ```bash 24 | php artisan migrate 25 | ``` 26 | 27 | ## Usage 28 | 29 | ### Creating a Survey 30 | 31 | Creating a new `Survey` is easy! You can build your survey fluently just like 32 | how you create all your `Eloquent` models in your app. 33 | ```php 34 | $survey = Survey::create(['name' => 'Cat Population Survey']); 35 | 36 | $survey->questions()->create([ 37 | 'content' => 'How many cats do you have?', 38 | 'type' => 'number', 39 | 'rules' => ['numeric', 'min:0'] 40 | ]); 41 | 42 | $survey->questions()->create([ 43 | 'content' => 'What\'s the name of your first cat', 44 | ]); 45 | 46 | $survey->questions()->create([ 47 | 'content' => 'Would you want a new cat?', 48 | 'type' => 'radio', 49 | 'options' => ['Yes', 'Oui'] 50 | ]); 51 | ``` 52 | 53 | See [the list of available question types](#question-types). 54 | 55 | #### Creating Multiple Sections 56 | You may also park your questions under multiple sections. 57 | ```php 58 | $survey = Survey::create(['name' => 'Cat Population Survey']); 59 | 60 | $one = $survey->sections()->create(['name' => 'Part One']); 61 | 62 | $one->questions()->create([ 63 | 'content' => 'How many cats do you have?', 64 | 'type' => 'number', 65 | 'rules' => ['numeric', 'min:0'] 66 | ]); 67 | 68 | $two = $survey->sections()->create(['name' => 'Part Two']); 69 | 70 | $two->questions()->create([ 71 | 'content' => 'What\'s the name of your first cat?', 72 | ]); 73 | 74 | $two->questions()->create([ 75 | 'content' => 'Would you want a new cat?', 76 | 'type' => 'radio', 77 | 'options' => ['Yes', 'Oui'] 78 | ]); 79 | ``` 80 | 81 | ### Creating an Entry 82 | 83 | #### From an Array 84 | The `Entry` model comes with a `fromArray` function. 85 | This is especially useful when you're creating an entry from a form submission. 86 | ```php 87 | (new Entry())->for($survey)->fromArray([ 88 | 'q1' => 'Yes', 89 | 'q2' => 5 90 | ])->push(); 91 | ``` 92 | 93 | The answer array should be in the format of `q + question_id => answer`, thus becoming `'q1' => 'my answer'`. 94 | 95 | #### By a Specific User 96 | You may fluently specify the participant using the `by()` function. 97 | ```php 98 | (new Entry())->for($survey)->by($user)->fromArray($answers)->push(); 99 | ``` 100 | 101 | ### Setting Constraints 102 | When creating your survey, you may set some constraints 103 | to be enforced every time a new `Entry` is being created. 104 | 105 | #### Allowing Guest Entries 106 | By default, `Entry` models require a `participant_id` when being created. 107 | If you wish to change this behaviour and accept guest entries, 108 | set the `accept-guest-entries` option on your `Survey` model. 109 | ```php 110 | Survey::create(['settings' => ['accept-guest-entries' => true]]); 111 | ``` 112 | 113 | #### Adjusting Entries Per Participant Limit 114 | All `Survey` models default to accept only **1 entry** per unique participant. 115 | You may adjust the `limit-per-participant` option on your `Survey` model 116 | or set it to `-1` to remove this limit altogether. 117 | ```php 118 | Survey::create(['settings' => ['limit-per-participant' => 1]]); 119 | ``` 120 | *Note that this setting will be ignored if the `accept-guest-entries` option is activated.* 121 | 122 | ### Validation 123 | 124 | #### Defining Validation Rules 125 | Add in a `rules` attribute when you're creating your `Question` to specify the validation logic 126 | for the answers being received. 127 | ```php 128 | Question::create([ 129 | 'content' => 'How many cats do you have?', 130 | 'rules' => ['numeric', 'min:0'] 131 | ]); 132 | ``` 133 | *Note that as opposed to the survey constraints, the question validators 134 | are not automatically triggered during the entry creation process. 135 | To validate the answers, you should manually run the validation in your controller (see below)* 136 | 137 | #### Validating Submissions 138 | Validate user's input against the entire rule set of your `Survey` using Laravel's built in validator. 139 | ```php 140 | class SurveyEntriesController extends Controller 141 | { 142 | public function store(Request $request, Survey $survey) 143 | { 144 | $answers = $request->validate($survey->rules); 145 | 146 | (new Entry())->for($survey)->fromArray($answers)->push(); 147 | } 148 | } 149 | ``` 150 | 151 | ### Views 152 | This package comes with boilerplate Bootstrap 4.0 views 153 | to display the surveys and some basic question types. 154 | These views are meant to serve as examples, and 155 | may not be sufficient for your final use case. 156 | To display a survey in a card, include the `survey` partial in your views: 157 | 158 | ```blade 159 | @include('survey::standard', ['survey' => $survey]) 160 | ``` 161 | 162 | #### Question Types 163 | These are the question types included out of the box: 164 | 165 | - `text` - Accepting text answers 166 | - `number` - Accepting numeric answers 167 | - `radio` - Options presented as radio buttons, accepting 1 option for the answer 168 | - `multiselect` - Options presented as checkboxes, accepting multiple options for the answer 169 | 170 | #### Customizing the Views 171 | To customize the boilerplate views shipped with this package run `package:publish` with the `views` tag. 172 | ```bash 173 | php artisan vendor:publish --provider="MattDaneshvar\Survey\SurveyServiceProvider" --tag="views" 174 | ``` 175 | This will create a new `vendor/survey` directory 176 | where you can fully customize the survey views to your liking. 177 | 178 | #### Creating New Question Types 179 | Once you publish the views that come with this package, you can add your own custom question types 180 | by implementing new templates for them. 181 | 182 | To implement a new `custom-select` type, for example, you should implement a new template under: 183 | 184 | ``` 185 | /vendor/survey/questions/types/custom-select.blade.php 186 | ``` 187 | 188 | ## License 189 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 190 | -------------------------------------------------------------------------------- /resources/views/partials/question.blade.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matt-daneshvar/laravel-survey/e188ec6916059d645abc3e54f36a3e3b20ee1e63/resources/views/partials/question.blade.php -------------------------------------------------------------------------------- /resources/views/questions/base.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{ $slot }} 4 | @if($errors->has($question->key)) 5 |
{{ $errors->first($question->key) }}
6 | @endif 7 |
8 | 9 |
10 | {{ $report ?? '' }} 11 |
12 | -------------------------------------------------------------------------------- /resources/views/questions/single.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @include(view()->exists("survey::questions.types.{$question->type}") 3 | ? "survey::questions.types.{$question->type}" 4 | : "survey::questions.types.text",[ 5 | 'disabled' => !($eligible ?? true), 6 | 'value' => $lastEntry ? $lastEntry->answerFor($question) : null, 7 | 'includeResults' => ($lastEntry ?? null) !== null 8 | ] 9 | ) 10 |
-------------------------------------------------------------------------------- /resources/views/questions/types/multiselect.blade.php: -------------------------------------------------------------------------------- 1 | @component('survey::questions.base', compact('question')) 2 | @foreach ($question->options as $option) 3 |
4 | key)) == $option ? 'checked' : '' }} 10 | {{ ($disabled ?? false) ? 'disabled' : '' }} 11 | > 12 | 15 |
16 | @endforeach 17 | @endcomponent 18 | -------------------------------------------------------------------------------- /resources/views/questions/types/number.blade.php: -------------------------------------------------------------------------------- 1 | @component('survey::questions.base', compact('question')) 2 | 4 | 5 | @slot('report') 6 | @if($includeResults ?? false) 7 | {{ number_format((new \MattDaneshvar\Survey\Utilities\Summary($question))->average()) }} (Average) 8 | @endif 9 | @endslot 10 | @endcomponent -------------------------------------------------------------------------------- /resources/views/questions/types/radio.blade.php: -------------------------------------------------------------------------------- 1 | @component('survey::questions.base', compact('question')) 2 | @foreach($question->options as $option) 3 |
4 | key)) == $option ? 'checked' : '' }} 10 | {{ ($disabled ?? false) ? 'disabled' : '' }} 11 | > 12 | 20 |
21 | @endforeach 22 | @endcomponent -------------------------------------------------------------------------------- /resources/views/questions/types/text.blade.php: -------------------------------------------------------------------------------- 1 | @component('survey::questions.base', compact('question')) 2 | 4 | @endcomponent -------------------------------------------------------------------------------- /resources/views/sections/single.blade.php: -------------------------------------------------------------------------------- 1 |

{{ $section->name }}

2 | @foreach($section->questions as $question) 3 | @include('survey::questions.single') 4 | @endforeach -------------------------------------------------------------------------------- /resources/views/standard.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ $survey->name }}

4 | 5 | @if(!$eligible) 6 | We only accept 7 | {{ $survey->limitPerParticipant() }} {{ \Str::plural('entry', $survey->limitPerParticipant()) }} 8 | per participant. 9 | @endif 10 | 11 | @if($lastEntry) 12 | You last submitted your answers {{ $lastEntry->created_at->diffForHumans() }}. 13 | @endif 14 | 15 |
16 | @if(!$survey->acceptsGuestEntries() && auth()->guest()) 17 |
18 | Please login to join this survey. 19 |
20 | @else 21 | @foreach($survey->sections as $section) 22 | @include('survey::sections.single') 23 | @endforeach 24 | 25 | @foreach($survey->questions()->withoutSection()->get() as $question) 26 | @include('survey::questions.single') 27 | @endforeach 28 | 29 | @if($eligible) 30 | 31 | @endif 32 | @endif 33 |
-------------------------------------------------------------------------------- /src/Casts/SeparatedByCommaAndSpace.php: -------------------------------------------------------------------------------- 1 | auth = $auth; 20 | } 21 | 22 | /** 23 | * Compose the view with relevant values. 24 | * 25 | * @param View $view 26 | */ 27 | public function compose(View $view) 28 | { 29 | $view->with([ 30 | 'eligible' => $view->survey->isEligible($this->auth->user()), 31 | 'lastEntry' => $view->survey->lastEntry(auth()->user()), 32 | ]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Models/Answer.php: -------------------------------------------------------------------------------- 1 | table)) { 21 | $this->setTable(config('survey.database.tables.answers')); 22 | } 23 | 24 | $this->casts['value'] = get_class(app(Value::class)); 25 | 26 | parent::__construct($attributes); 27 | } 28 | 29 | /** 30 | * The attributes that are mass assignable. 31 | * 32 | * @var array 33 | */ 34 | protected $fillable = ['value', 'question_id', 'entry_id']; 35 | 36 | /** 37 | * The entry the answer belongs to. 38 | * 39 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 40 | */ 41 | public function entry() 42 | { 43 | return $this->belongsTo(get_class(app()->make(Entry::class))); 44 | } 45 | 46 | /** 47 | * The question the answer belongs to. 48 | * 49 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 50 | */ 51 | public function question() 52 | { 53 | return $this->belongsTo(get_class(app()->make(Question::class))); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Models/Entry.php: -------------------------------------------------------------------------------- 1 | validateParticipant(); 34 | $entry->validateMaxEntryPerUserRequirement(); 35 | }); 36 | } 37 | 38 | /** 39 | * Entry constructor. 40 | * 41 | * @param array $attributes 42 | */ 43 | public function __construct(array $attributes = []) 44 | { 45 | if (! isset($this->table)) { 46 | $this->setTable(config('survey.database.tables.entries')); 47 | } 48 | 49 | parent::__construct($attributes); 50 | } 51 | 52 | /** 53 | * The answers within the entry. 54 | * 55 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 56 | */ 57 | public function answers() 58 | { 59 | return $this->hasMany(get_class(app()->make(Answer::class))); 60 | } 61 | 62 | /** 63 | * The survey the entry belongs to. 64 | * 65 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 66 | */ 67 | public function survey() 68 | { 69 | return $this->belongsTo(get_class(app()->make(Survey::class))); 70 | } 71 | 72 | /** 73 | * The participant that the entry belongs to. 74 | * 75 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 76 | */ 77 | public function participant() 78 | { 79 | return $this->belongsTo(User::class, 'participant_id'); 80 | } 81 | 82 | /** 83 | * Set the survey the entry belongs to. 84 | * 85 | * @param Survey $survey 86 | * @return $this 87 | */ 88 | public function for(Survey $survey) 89 | { 90 | $this->survey()->associate($survey); 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * Set the participant who the entry belongs to. 97 | * 98 | * @param Model|null $model 99 | * @return $this 100 | */ 101 | public function by(?Model $model = null) 102 | { 103 | $this->participant()->associate($model); 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * Create an entry from an array. 110 | * 111 | * @param array $values 112 | * @return $this 113 | */ 114 | public function fromArray(array $values) 115 | { 116 | foreach ($values as $key => $value) { 117 | if ($value === null) { 118 | continue; 119 | } 120 | 121 | $answer_class = get_class(app()->make(Answer::class)); 122 | 123 | $this->answers->add($answer_class::make([ 124 | 'question_id' => substr($key, 1), 125 | 'entry_id' => $this->id, 126 | 'value' => $value, 127 | ])); 128 | } 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * The answer for a given question. 135 | * 136 | * @param Question $question 137 | * @return mixed|null 138 | */ 139 | public function answerFor(Question $question) 140 | { 141 | $answer = $this->answers()->where('question_id', $question->id)->first(); 142 | 143 | return isset($answer) ? $answer->value : null; 144 | } 145 | 146 | /** 147 | * Save the model and all of its relationships. 148 | * Ensure the answers are automatically linked to the entry. 149 | * 150 | * @return bool 151 | */ 152 | public function push() 153 | { 154 | $this->save(); 155 | 156 | foreach ($this->answers as $answer) { 157 | $answer->entry_id = $this->id; 158 | } 159 | 160 | return parent::push(); 161 | } 162 | 163 | /** 164 | * Validate participant's legibility. 165 | * 166 | * @throws GuestEntriesNotAllowedException 167 | */ 168 | public function validateParticipant() 169 | { 170 | if ($this->survey->acceptsGuestEntries()) { 171 | return; 172 | } 173 | 174 | if ($this->participant_id !== null) { 175 | return; 176 | } 177 | 178 | throw new GuestEntriesNotAllowedException(); 179 | } 180 | 181 | /** 182 | * Validate if entry exceeds the survey's 183 | * max entry per participant limit. 184 | * 185 | * @throws MaxEntriesPerUserLimitExceeded 186 | */ 187 | public function validateMaxEntryPerUserRequirement() 188 | { 189 | $limit = $this->survey->limitPerParticipant(); 190 | 191 | if ($limit === null) { 192 | return; 193 | } 194 | 195 | $count = static::where('participant_id', $this->participant_id) 196 | ->where('survey_id', $this->survey->id) 197 | ->count(); 198 | 199 | if ($count >= $limit) { 200 | throw new MaxEntriesPerUserLimitExceeded(); 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/Models/Question.php: -------------------------------------------------------------------------------- 1 | 'array', 22 | 'options' => 'array', 23 | ]; 24 | 25 | /** 26 | * Boot the question. 27 | * 28 | * @return void 29 | */ 30 | protected static function boot() 31 | { 32 | parent::boot(); 33 | 34 | //Ensure the question's survey is the same as the section it belongs to. 35 | static::creating(function (self $question) { 36 | $question->load('section'); 37 | 38 | if ($question->section) { 39 | $question->survey_id = $question->section->survey_id; 40 | } 41 | }); 42 | } 43 | 44 | /** 45 | * Question constructor. 46 | * 47 | * @param array $attributes 48 | */ 49 | public function __construct(array $attributes = []) 50 | { 51 | if (! isset($this->table)) { 52 | $this->setTable(config('survey.database.tables.questions')); 53 | } 54 | 55 | parent::__construct($attributes); 56 | } 57 | 58 | /** 59 | * The survey the question belongs to. 60 | * 61 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 62 | */ 63 | public function survey() 64 | { 65 | return $this->belongsTo(get_class(app()->make(Survey::class))); 66 | } 67 | 68 | /** 69 | * The section the question belongs to. 70 | * 71 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 72 | */ 73 | public function section() 74 | { 75 | return $this->belongsTo(get_class(app()->make(Section::class))); 76 | } 77 | 78 | /** 79 | * The answers that belong to the question. 80 | * 81 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 82 | */ 83 | public function answers() 84 | { 85 | return $this->hasMany(get_class(app()->make(Answer::class))); 86 | } 87 | 88 | /** 89 | * The question's validation rules. 90 | * 91 | * @param $value 92 | * @return array|mixed 93 | */ 94 | public function getRulesAttribute($value) 95 | { 96 | $value = $this->castAttribute('rules', $value); 97 | 98 | return $value !== null ? $value : []; 99 | } 100 | 101 | /** 102 | * The unique key representing the question. 103 | * 104 | * @return string 105 | */ 106 | public function getKeyAttribute() 107 | { 108 | return "q{$this->id}"; 109 | } 110 | 111 | /** 112 | * Scope a query to only include questions that 113 | * don't belong to any sections. 114 | * 115 | * @param $query 116 | * @return mixed 117 | */ 118 | public function scopeWithoutSection($query) 119 | { 120 | return $query->where('section_id', null); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Models/Section.php: -------------------------------------------------------------------------------- 1 | table)) { 19 | $this->setTable(config('survey.database.tables.sections')); 20 | } 21 | 22 | parent::__construct($attributes); 23 | } 24 | 25 | /** 26 | * The attributes that are mass assignable. 27 | * 28 | * @var array 29 | */ 30 | protected $fillable = ['name']; 31 | 32 | /** 33 | * The questions of the section. 34 | * 35 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 36 | */ 37 | public function questions() 38 | { 39 | return $this->hasMany(get_class(app()->make(Question::class))); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Models/Survey.php: -------------------------------------------------------------------------------- 1 | table)) { 21 | $this->setTable(config('survey.database.tables.surveys')); 22 | } 23 | 24 | parent::__construct($attributes); 25 | } 26 | 27 | /** 28 | * The attributes that are mass assignable. 29 | * 30 | * @var array 31 | */ 32 | protected $fillable = ['name', 'settings']; 33 | 34 | /** 35 | * The attributes that should be casted. 36 | * 37 | * @var array 38 | */ 39 | protected $casts = [ 40 | 'settings' => 'array', 41 | ]; 42 | 43 | /** 44 | * The survey sections. 45 | * 46 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 47 | */ 48 | public function sections() 49 | { 50 | return $this->hasMany(get_class(app()->make(Section::class))); 51 | } 52 | 53 | /** 54 | * The survey questions. 55 | * 56 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 57 | */ 58 | public function questions() 59 | { 60 | return $this->hasMany(get_class(app()->make(Question::class))); 61 | } 62 | 63 | /** 64 | * The survey entries. 65 | * 66 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 67 | */ 68 | public function entries() 69 | { 70 | return $this->hasMany(get_class(app()->make(Entry::class))); 71 | } 72 | 73 | /** 74 | * Check if survey accepts guest entries. 75 | * 76 | * @return bool 77 | */ 78 | public function acceptsGuestEntries() 79 | { 80 | return $this->settings['accept-guest-entries'] ?? false; 81 | } 82 | 83 | /** 84 | * The maximum number of entries a participant may submit. 85 | * 86 | * @return int|null 87 | */ 88 | public function limitPerParticipant() 89 | { 90 | if ($this->acceptsGuestEntries()) { 91 | return; 92 | } 93 | 94 | $limit = $this->settings['limit-per-participant'] ?? 1; 95 | 96 | return $limit !== -1 ? $limit : null; 97 | } 98 | 99 | /** 100 | * Survey entries by a participant. 101 | * 102 | * @param Model $participant 103 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 104 | */ 105 | public function entriesFrom(Model $participant) 106 | { 107 | return $this->entries()->where('participant_id', $participant->id); 108 | } 109 | 110 | /** 111 | * Last survey entry by a participant. 112 | * 113 | * @param Model $participant 114 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 115 | */ 116 | public function lastEntry(?Model $participant = null) 117 | { 118 | return $participant === null ? null : $this->entriesFrom($participant)->first(); 119 | } 120 | 121 | /** 122 | * Check if a participant is eligible to submit the survey. 123 | * 124 | * @param Model|null $model 125 | * @return bool 126 | */ 127 | public function isEligible(?Model $participant = null) 128 | { 129 | if ($participant === null) { 130 | return $this->acceptsGuestEntries(); 131 | } 132 | 133 | if ($this->limitPerParticipant() === null) { 134 | return true; 135 | } 136 | 137 | return $this->limitPerParticipant() > $this->entriesFrom($participant)->count(); 138 | } 139 | 140 | /** 141 | * Combined validation rules of the survey. 142 | * 143 | * @return mixed 144 | */ 145 | public function getRulesAttribute() 146 | { 147 | return $this->questions->mapWithKeys(function ($question) { 148 | return [$question->key => $question->rules]; 149 | })->all(); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/SurveyServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 20 | __DIR__.'/../config/survey.php' => config_path('survey.php'), 21 | ], 'config'); 22 | 23 | $this->publishes([ 24 | __DIR__.'/../resources/views/' => base_path('resources/views/vendor/survey'), 25 | ], 'views'); 26 | 27 | $this->mergeConfigFrom(__DIR__.'/../config/survey.php', 'survey'); 28 | 29 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'survey'); 30 | 31 | $viewFactory->composer('survey::standard', SurveyComposer::class); 32 | 33 | $this->publishMigrations([ 34 | 'create_surveys_table', 35 | 'create_questions_table', 36 | 'create_entries_table', 37 | 'create_answers_table', 38 | 'create_sections_table', 39 | ]); 40 | } 41 | 42 | /** 43 | * Register the application services. 44 | * 45 | * @return void 46 | */ 47 | public function register() 48 | { 49 | $this->app->bind(\MattDaneshvar\Survey\Contracts\Answer::class, \MattDaneshvar\Survey\Models\Answer::class); 50 | $this->app->bind(\MattDaneshvar\Survey\Contracts\Entry::class, \MattDaneshvar\Survey\Models\Entry::class); 51 | $this->app->bind(\MattDaneshvar\Survey\Contracts\Question::class, \MattDaneshvar\Survey\Models\Question::class); 52 | $this->app->bind(\MattDaneshvar\Survey\Contracts\Section::class, \MattDaneshvar\Survey\Models\Section::class); 53 | $this->app->bind(\MattDaneshvar\Survey\Contracts\Survey::class, \MattDaneshvar\Survey\Models\Survey::class); 54 | $this->app->bind(\MattDaneshvar\Survey\Contracts\Value::class, \MattDaneshvar\Survey\Casts\SeparatedByCommaAndSpace::class); 55 | } 56 | 57 | /** 58 | * Publish package migrations. 59 | * 60 | * @param $migrations 61 | */ 62 | protected function publishMigrations($migrations) 63 | { 64 | foreach ($migrations as $migration) { 65 | $migrationClass = Str::studly($migration); 66 | 67 | if (class_exists($migrationClass)) { 68 | return; 69 | } 70 | 71 | $this->publishes([ 72 | __DIR__."/../database/migrations/$migration.php.stub" => database_path('migrations/'.date('Y_m_d_His', 73 | time())."_$migration.php"), 74 | ], 'migrations'); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Utilities/Summary.php: -------------------------------------------------------------------------------- 1 | question = $question; 24 | } 25 | 26 | /** 27 | * Find all answers with the same value. 28 | * 29 | * @param $value 30 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 31 | */ 32 | public function similarAnswers($value) 33 | { 34 | return $this->question->answers()->where('value', $value); 35 | } 36 | 37 | /** 38 | * Find the ratio of similar answers to all other answers. 39 | * 40 | * @param $value 41 | * @return float|int 42 | */ 43 | public function similarAnswersRatio($value) 44 | { 45 | $total = $this->question->answers()->count(); 46 | 47 | return $total > 0 ? $this->similarAnswers($value)->count() / $total : 0; 48 | } 49 | 50 | /** 51 | * Calculate the average answer. 52 | * 53 | * @return int|mixed 54 | */ 55 | public function average() 56 | { 57 | return $this->question->answers()->average('value') ?? 0; 58 | } 59 | } 60 | --------------------------------------------------------------------------------