A package for building surveys using xml and rendering them to html in Laravel projects.
It's helpful to understand the concepts behind the sirs/surveys package before diving into the technical details.
- SurveyDocument: An XML document that describes the structure of a survey using the survey.xsd schema.
- Survey: The model that represents surveys stored in the database
- Response: The model that represents a survey response as stored in the database.
- Respondent: The model in the application that is responding to the survey's questions. The model that the response should be associated with.
- Rules: A class that defines how the survey behaves.
- WorkflowStrategies: A class that is called when a SurveyResponseEvent for a particular survey is fired.
- Install the package
- Configure: update /config/surveys.php
- Create Survey definition directory: /resources/surveys
- Create custom template directory: /resources/views/surveys
- write your first survey and save in /resources/surveys ** See survey definition schema docs in the wiki
- Run
php artisan survey:new <path_to_survey>to create a migration and rules file - To replace the migration run
php artisan survey:migration <path_to_survey>
- run
composer require sirs/surveys - add the service provider to config/app.php:
Sirs\Surveys\SurveysServiceProvider::class, - add the service provider to your app config:
Sirs\Surveys\SurveysServiceProvider::class, - Publish the stylesheets and config file:
$ php artisan vendor:publish
- Because the Response model refers to a number of different response tables the best way to interact with responses is through survyeys:
$respondent = new Participant(); $survey = class_survey()::findBySlug('svyslug'); //see bindings in configuration for info on class_path() $responses = $survey->responses; $newResponse = $survey->getNewResponse($respondent); - New responses can be accessed via the route
/{full-respondent-class-name}/{id}/survey/{survey-slug}/new` i.e. `app-participant/1/survey/screener1/new - Existing response can be accessed via the route
/{full-respondent-class-name}/{id}/survey/screener1/{response_id}` - Latest existing or new response can be accessed via the route
/{full-respondent-class-name}/{id}/survey/screener1
-
editAfterFinalized : (bool) - Allow users to edit responses after they've been marked finalized
-
surveysPath: (string) - Path to survey XML documents
-
rulesPath: (string) - Path to rules classes
-
rulesNamespace: (string) - Namespace of rules classes
-
customTemplatePath: (string) - Path to custom templatesREMOVED -
rendererConfig:
- cache_path: (string) - Path to view cache
-
routeGroup: (array) - Route group to apply to survey routes
-
chromeTemplate: (string) - Blade template to use as chrome for survey views
-
cacheDocuments: (bool) -
-
refusedLabel: (string) - Label to use for refused options on refusable questions
-
autosave:
- enabled: (bool) - Enable/disable autosaving,
- frequency: (int) - Frequency of autosaves in milliseconds
- notify: (bool) - Notify the user of the autosave
- notify_time: (int) - Length of time the notification is displayed
-
defualt_templates: (array) - An array defining default templates for blocks.
-
validation_messages: (array) - An array of Laravel validatition custom messages. You can customize the message for a rule globally or for a specific field as per the Laravel Validation Documentation
-
bindings: Bindings can be used to override the classes used for the Survey and Response models, allowing you to do things like define relationships, fire custom events, and override default behavior. Leave this section of the config commented out to use the models provided by the package.
NOTE: This is optional. If not set Sirs\Surveys\Models\Response and Sirs\Surveys\Models\Survey will be used.- models:
- Survey: (string) - App\Survey::class (must implement Sirs\Surveys\Contracts\SurveyContract)
- the
class_surve()will always return the configured Survey model. Usingclass_survey::methond()instead of\Sirs\Surveys\Models\Surveys::method()is recommended for future-proofing your application from changing configurations.
- the
- Response: (string) - App\SurveyResponse::class (must implement Sirs\Surveys\Contracts\SurveyResponse)
- Survey: (string) - App\Survey::class (must implement Sirs\Surveys\Contracts\SurveyContract)
- models:
make:survey- Creates a stub survey filemake:survey-rules- Create a new survey rules classsurvey:migration- Create/update migrations from survey documentsurvey:migrations-all- Rebuild migrations for all surveys in the project.survey:new- Creates new survey migration and rules documentsurvey:refresh- Refresh response tablessurvey:rules- Create/update rules file from survey documentsurvey:validate- Validate a survey against the schemasurvey:workflow- Create SurveyWorkflowStrategy class for survey of type
Run php artisan list | grep survey for a complete list of artisan commands related to surveys. Run php artisan help <command> for details on commands.
You can create surveys 2 ways
- All at once:
- Create XML stub and Rules:
$ php artisan make:survey survey_name - Add questions to your survey.
- Create migration:
$ php artisan survey:migration path/to/survey/survey_name.xml
- Create XML stub and Rules:
- By hand:
- Add survey XML document to your surveysPath (see configuration)
- Create a rules class and a migration with
$ php artisan survey:new path/to/survey/document- Alternately you can run those seperately:
- Create rules:
$ php artisan survey:rules path/to/survey/document - Create migration:
$ php artisan survey:migration path/to/survey/document
- Create rules:
- Alternately you can run those seperately:
Once you've created a rules class for your survey you can begin defining behavior. The rules class supports a number of methods that allow you to manipulate the behavior of the entire survey or a specific page.
- $response - resolves to instance of Sirs\Surveys\Models\Response or binding specified in the config.
- $survey - resolves to instance of Sirs\Surveys\Models\Survey or binding specified in the config.
- Mixed $respondent - The response's respondent
- beforeShow(void):array $context - executed before showing a page. Allows you to add data to context passed to the view before rendering:
$context['beans'] = 'monkeys'; - beforeSave(void) - Executes logic before the response data is saved.
- afterSave(void) - Executes logic after the response data is saved.
- navigate(['page'=>'pageName', 'nav'=>'navType']):int|null - Allows for custom navigation behavior on 'next' or 'prev' navigation. Return a page number to override the default navigation; return null for default navigation behavior.
- getRedirectUrl(void) - Gets url for redirection when exiting survey (i.e. Save & Exit or Finalize )
- Advanced setPretext(array $requestData) - Stores info from request to the session
- Advanced forgetPretext() - Clears pretext from session
- [pageName]BeforeShow() - Same as beforeShow() but for page with name pageName
- [pageName]BeforeSave() - Same as beforeSave() but for page with name pageName
- [pageName]AfterSave() - Same as afterSave() but for page with name pageName
- [pageName]Skip():int - Allows skiping of page with pageName. Called before showing page. Returns integer code for behavior:
- 0: default behavior - show the page
- 1: skip page - skip this page and proceed with normal navigation i.e. next/prev
- 2: finalize - skip this page, finalize the response, and redirect out of survey
- 3: exit - exit survey without finalizing.
- [PageName]GetValidator(Validator $validator):Validator $validator - Augment the validation defined in the xml with additional rules. Should return validtor object.
There are several ways to reuse rule logic across surveys:
- Set the rules-class attribute to the FQNS of the rules class you want to use.
- Use traits to reuse rules for pages that appear in more than one survey
- Inherit from a rules class.
Survey responses fire the following events:
- SurveyResponseStarted - fired when a response's started_at field is updated from null.
- SurveyResponseSaved - fired when a response is saved.
- SurveyResponseFinalized - fired when a response's finalized_at attribute is updated from null.
- SurveyResponseReopened - fired when a response's finalized_at attribute is updated from a timestamp to null
In addition to creating your own listeners for these events, sirs/surveys lets you create 'WorkflowStrategies' for responses to specfic surveys. A WorkflowStrategy lets you define all of your event triggered logic for a survey's responses in one file. You can think of a WorkflowStrategy like an observer for a survey response for a particular survey.
To scaffold a WorkflowStrategy run $ php artisan survey:workflow survey-slug where survey-slug is the slug for the survey stored in the surveys table.
You'll find your new workflow strategy at /path/to/SurveyRules/Workflows/SurveySlugWorkflowStrategy, with methods for handling each SurveyResponseEvent ready to go.
Each WorkflowStrategy class has the following attributes:
- $response - The response that triggered the event.
- $event - The event that was triggered.
There are several was to customize the surveys package
Survey package templates use the surveys:: blade namespace (i.e. surveys::questions.multiple_choice.select).
To specify a template as the default for a block type use the surveys.default_templates config.
Alternately, you can override default templates by adding a template with the same relative path to resources/views/vendor/surveys. For example, if you wanted to override the default page template, simply add resources/views/vendor/surveys/containers/page/page.blade.php to your project.
Every element of a survey that can be rendered extends the RenderableBlock class. Within the template used to render a RenderableBlock you have access to the following variables:
- $context - The context populated by the survey package and/or the rules class beforeShow method(s). By default $context includes
- $response - the survey response
- $survey - an array with summary information about the survey
- $renderable - the block the template is rendering. For example, in the template for a multiple-choice question $renderable is the QuestionBlock object; in a template for a question-group $renderable is the question-group block
With access to the block being rendered you have everything you need to render the block and it's children. In the case of containers (blocks that can contain other blocks) you can access their children as an associative array with the block name as the key at $renderable->contents.
See example xml and blade template
In addition to checking for views in surveys:: views namespace, the package will also check to see if the specified custom template exists in the default view namespace. This is currently included to support projects using pre-4.0 custom template organization.
See examples in source examples dir
Containers are blocks that have other elements as contents.
- name - string - required|unique: This is used as the variable/column name
- id - string: Analogous to html attribute
- class - string: Analogous to html attribute
Specifies an alternate template to use for the block.
<template source="path.to.template"></template>
Allows for the definition of additional information about the renderable block.
Requires one or more <datum> children. Keys and values can be defined as attributes or children of <datum> tag
<metadata>
<datum key="datum_key" value="datum-value"></datum>
<datum>
<key>Another Key</key>
<value>
<![CDATA[my cdata-requiring value here.]]>
</value>
</datum>
</metadata>
- QuestionGroup
- Likert
- Html
- Question
The page can contain 1 or more child blocks that are displayed as a page.
- containers.page.page (default)
An arbitrary grouping of other elements
- QuestionGroup
- Likert
- Html
- Question
- block_default (default) - Container that renders each child.
A specialized container for applying a single set of options to a number of questions.
- Prompt - Prompt for the set of questions
- question - Any number of question elements
- options - A group of options that are used for all questions in the likert
- containers.likert.btn_group_likert (default) - Bootstrap button based likert display.
- containers.likert.traditional_likert - Traditional likert w/ radio buttons and option labels at top.
The root question: expects a string based response by default.
<question name="name" id="name">
<question-text>question-text</question-text>
</question>
- name - string - required|unique: This is used as the variable/column name
- id - string: Analogous to html attribute
- class - string: Analogous to html attribute
- placeholder - string: Analogous to html attribute
- disabled - boolean [false]: Analogous to html attribute
- readonly - boolean [false]: Analogous to html attribute
- hidden - boolean [false]: Indicates question should be hidden
- refusable - boolean [false]: Indicates the respondent should be able to mark the question as refused to answer;
- refuse-label - string [refused]: Label used for refusable questions
- data-format - string [varchar]: Data type used for sql column. All valid mysql column types supported.
- validation-rules - string: Laravel validation rules string.
The question prompt or text.
<question-text>Question text goes here</question-text>
Specifies an alternate template to use for rendering the block.
<template source="path.to.template"></template>
Allows for the definition of additional information about the question. Requires one or more <datum> children. Keys and values can be defined as attributes or children of <datum> tag
<metadata>
<datum key="datum_key" value="datum-value"></datum>
<datum>
<key>Another Key</key>
<value>
<![CDATA[my cdata-requiring value here.]]>
</value>
</datum>
</metadata>
- questions.text.default_text (default) - text input
- questions.text.inline - inline text input
- questions.large_text - textarea (should be used with
data-format="text"or similar, or validated to prevent response truncation when stored) - questions.other_field - inline text input for use as other details.
A question with a set number of possible answers. MulipleChoice questions can be single-select or multi-select (if num-selectable attribute is greater than 1).
<multiple-choice name="my_mc" id="my_mc" num-selectable="1">
<question-text>
<![CDATA[What is your favorite color]]>
</question-text>
<options>
<option name="my_mc1">
<value>1</value>
<label>blue</label>
</option>
<option name="my_mc2">
<value>2</value>
<label>green</label>
</option>
<option name="my_mc3">
<value>3</value>
<label>yellow</label>
</option>
<option name="my_mc4">
<value>4</value>
<label>red</label>
</option>
<option name="my_mc5">
<value>5</value>
<label>black</label>
</option>
</options>
</multiple-choice>
- num-selectable - integer [1]: Number of options the respondent can select.
- options - takes a list of option tags or a data-source tag. data-source's URI attribute supports a api endpoint URL, a function, or a class method using laravel's 'action' syntax (ClassName@method). For methods and functions parameters can be passed in the format
ClassName@method:param1=val1,param2=val2
- name - string - required
- id
- class
- exclusive - integer: integer indicates group
Allows for the definition of additional information about the renderable block.
Requires one or more <datum> children. Keys and values can be defined as attributes or children of <datum> tag
<metadata>
<datum key="datum_key" value="datum-value"></datum>
<datum>
<key>Another Key</key>
<value>
<![CDATA[my cdata-requiring value here.]]>
</value>
</datum>
</metadata>
- questions.multiple_choice.radio_group (default) - bootstrap button style radio buttons
- questions.multiple_choice.radio_group_vertical - radio buttons
- questions.multiple_choice.select - html select
- questions.multiple_choice.checkbox_group (default - multi-select) - checkboxes
- questions.multiple_choice.select - html select
A question which requires a number for a value between min to max.
<number name="Height" required="1" data-format="int">
<template source="Height_Inches.blade.php" />
<question-text>Height (inches)</question-text>
</number>
A numeric-scale with options ranging from min to max at interval. (Extends Number)
<numeric-scale name="test" min="1" max="5" interval="1">
<question-text>Numeric scale question.</question-text>
<legend>
<item>
<label>First</label>
<value>1</value>
</item>
<item>
<label>Last</label>
<value>5</value>
</item>
</legend>
</numeric-scale>
- min - interger - required
- max - integer - required
- interval - integer - interval between options - default: 1
- reverse - boolean - if 1 list scale in reverse order
The legend describes the meaning of the options. It is made up of any number of items each of which have a <label> and <value>. When rendered the items will be evenly distributed above the options.
A question collecting date information between optional min and max boundaries
<date name="DOB" required="1" placeholder="MM/DD/YYYY" min="1940-01-01" max="1990-01-01">
<question-text>Date of Birth</question-text>
</date>
Validates against min and max attributes if set.
- min - date in the format of 'yyyy-mm-dd'
- max - date in the format of 'yyyy-mm-dd'
- questions.date - input w/ date picker
Single-select multiple choice question with months (1-12) as options.
- questions.multiple_choice.select (default)
- Any template that supports single-select multiple-choice questions
Numeric scale question for years as options.
<year min="now" max="+30 years">
<question-text>When will you retire?</question-text>
</year>
- min - parsable date string (i.e. '2019-01-01', '09/16/1977', 'tomorrow', '+1 year')
- max - parsable date string (i.e. '2019-01-01', '09/16/1977', 'tomorrow', '+1 year')
- questions.multiple_choice.select (default)
- Any template that supports single-select multiple-choice questions
A question for collected time information.
Validates against min and max attributes if set.
- min - time in the format of 'hr:min:sec
- max - time in the format of 'hr:min:sec
- questions.time - input w/ time picker
- TJ Ward - jward3@email.unc.edu
- Alex Harding - ahhardin@email.unc.edu