Ранее на Jobeet
Вчера мы изучали как Symfony упрощает управление БД, абстрагируя разницу между движками баз данных и конвертируя связанные элементы в прекрасные объекто-ориентированные классы. Мы уже также успели пощупать Propel, описывая схему базы данных, создавая таблицы и заводя некоторые начальные данные.
Сегодня мы будем изменять базовый модуль job
, созданный вчера.
Модуль job
уже имеет весь необходимый код для Jobeet:
- Страницу со списком вакансий
- Страницу создания вакансии
- Страницу обновления вакансии
- Страницу удаления вакансии
Поскольку код готов к использования "как есть", а мы будем рефакторить шаблоны, чтобы они стали более похожи на эскизы Jobeet.
Архитектура MVC
Если Вы привыкли создавать сайты на PHP без применения фреймворка, то возможно Вы используете парадигму, в которой один PHP-файл соответствует одной веб-странице. Возможно данные PHP-файлы содержат схожую структуру: инициализация и глобальные настройки, бизнес-логика привязана к текущей странице, получение записей из БД и конечно же HTML код, который выстраивает страницу.
Возможно Вы используете шаблонизатор для отделения логики от HTML. Возможно Вы используете слой абстракции баз данных, для отделения моделей от бизнес-логики. Но в большинстве случаев всё заканчивается огромным количеством кода, поддерживать который - сущий кошмар. Создать было просто, а вот вносить новые изменения становится все труднее и труднее, особенно если никто, кроме Вас, не понимает как это сделано и как это работает.
Для каждой проблемы всегда есть красивое решение. Для веб-разработок одним из лучших способов организации кода в наше время является шаблон проектирования MVC. Если коротко, то шаблон проектирования MVC определяет способ организации вашего кода в зависимости от его назначения. Шаблон разделяет код на три слоя:
Model (Модель) - определяет бизнес-логику (база данных относится к данному слою). Вы уже знаете о том, что Symfony хранит все классы, связанные с моделью в каталоге
lib/model
.View (Представление) - это то, с чем работает пользователь (шаблонный движок являются частью данного слоя). В Symfony представления по большей части основаны на PHP-шаблонах. Они хранятся в различных каталогах
templates
, в чём мы убедимся чуть позже.Controller (Контроллер) - это часть кода, которая вызывает модель для получения данных, которые уже передаёт представлению для их обработки и выдачи клиенту. Когда мы установили Symfony в первый день, то мы увидели, что все запросы принимаются фронт-контроллерами (
index.php
иfrontend_dev.php
). Данные фронт-контроллеры делегируют обработку контроллерам (actions). Вчера мы узнали, что действия логически сгруппированы в модули.
Сегодня мы воспользуемся эскизами, созданными на второй день, для настройки внешнего вида главной страницы и страницы вакансий и сделаем их динамичными. По ходу дела мы изменим несколько вещей в небольших файлах, чтобы показать файловую структуру Symfony, а так же способ разделения кода между слоями MVC.
Обёртка
Взлянув повнимательней на наши наброски, Вы наверное заметили, что некоторые вещи на страницах выглядят абсолютно одинаково. Вы уже знаете, что повторение кода это моветон, назависимо от того говорим ли мы о PHP или HTML, поэтому нам нужно найти способ предотвратить повторение данного кода.
Одним из путей решения этой проблемы является определение заголовка (header) и подвала страницы (footer) и вставка их в каждый из шаблонов.
Только вот в данном случае файлы заголовка и подвала не содержат верного HTML. Должен быть лучший способ. Вместо изобретения велосипеда, мы используем другой дизайн-шаблон для решения проблемы: Декоратор (Decorator). Декотратор решает проблему обходным путём: шаблон обрамляется другим общим шаблоном, после того, как готово всё содержимое. Данный шаблон называется в Symfony основным шаблоном (layout):
Стандартный шаблон в приложении называется layout.php
и может быть
найден в каталоге apps/frontend/templates/
. Данный каталог содержит все
общие (глобальные) шаблоны приложения.
Замените текущий шаблон Symfony следующим кодом:
<!-- apps/frontend/templates/layout.php --> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <title>Jobeet - Your best job board</title> <link rel="shortcut icon" href="/favicon.ico" /> <?php include_javascripts() ?> <?php include_stylesheets() ?> </head> <body> <div id="container"> <div id="header"> <div class="content"> <h1><a href="/job"> <img src="/legacy/images/jobeet.gif" alt="Jobeet Job Board" /> </a></h1> <div id="sub_header"> <div class="post"> <h2>Ask for people</h2> <div> <a href="/job/new">Post a Job</a> </div> </div> <div class="search"> <h2>Ask for a job</h2> <form action="" method="get"> <input type="text" name="keywords" id="search_keywords" /> <input type="submit" value="search" /> <div class="help"> Enter some keywords (city, country, position, ...) </div> </form> </div> </div> </div> </div> <div id="content"> <?php if ($sf_user->hasFlash('notice')): ?> <div class="flash_notice"><?php echo $sf_user->getFlash('notice') ?></div> <?php endif; ?> <?php if ($sf_user->hasFlash('error')): ?> <div class="flash_error"><?php echo $sf_user->getFlash('error') ?></div> <?php endif; ?> <div class="content"> <?php echo $sf_content ?> </div> </div> <div id="footer"> <div class="content"> <span class="Symfony"> <img src="/legacy/images/jobeet-mini.png" /> powered by <a href="http://www.Symfony-project.org/"> <img src="/legacy/images/Symfony.gif" alt="Symfony framework" /></a> </span> <ul> <li><a href="">About Jobeet</a></li> <li class="feed"><a href="">Full feed</a></li> <li><a href="">Jobeet API</a></li> <li class="last"><a href="">Affiliates</a></li> </ul> </div> </div> </div> </body> </html>
Шаблон Symfony это обычный PHP-файл. В основном шаблоне Вы увидите вызовы
PHP функций и ссылки на переменные. $sf_content
самая интересная переменная -
она определяется фреймворком и содержит весь HTML-код, созданный действием контроллера.
Если Вы взглянете теперь на модуль job
(http://jobeet.localhost/frontend_dev.php/job
),
то увидите, что все действия теперь обрамлены шаблоном.
note
В шаблоне мы прописали favicon. Вы можете
скачать иконку Jobeet
и разместить её в каталоге web/
.
Стили, изображение, Javascript
Мы подготовили самый базовый дизайн для текущего использования:
здесь можно скачать изображения
и разместить их в директорию web/legacy/images/
;
вот тут файлы стилей,
которые стоит разместить в директорию web/css/
.
tip
Команда generate:project
создала для нас 3 директории для файлов проекта:
web/legacy/images/
для изображений, web/css/
для стилей и
web/js/
для JavaScript'ов. Это одно из нескольких правил, определяемых Symfony.
Но никто Вам не запрещает хранить файлы в любом месте каталога web/
Внимательный читатель скорей всего заметил, что даже файл main.css
,
который нигде не упоминается в шаблоне, появляется в полученном HTML/
А вот другие стили - нет. Как такое возможно?
Файлы стилей встраиваются при помощи функции include_stylesheets()
,
вызов которой можно обнаружить в теге <head>
. Функция include_stylesheets()
называется помощник (helper). Помощник это функция, определённая в Symfony, которая принимает
параметры и возвращает HTML код. В большинстве случаев помощники помогают экономить время,
они содержать небольшие кусочки кода, переодически используемые в шаблонах.
Помощник include_stylesheets()
создаёт тег <link>
для стилей.
Но откуда помощнику известно, какие файл стилей нужно использовать?
Представление может быть настроено при помощи редактирования файла конфигурации приложения
view.yml
. Вот стандартное содержимое файла, получаемое после использования команды
generate:app
:
# apps/frontend/config/view.yml default: http_metas: content-type: text/html metas: #title: Symfony project #description: Symfony project #keywords: Symfony, project #language: en #robots: index, follow stylesheets: [main.css] javascripts: [] has_layout: on layout: layout
Файл view.yml
определяет стандартные
настройки для всех шаблонов
приложения. Например, опция stylesheets
определяет массив файлов стилей,
которые будут включены на каждой странице приложения (включение осуществляется
при помощи помощника include_stylesheets()
).
note
В стандартном файле view.yml
идёт ссылка на main.css
, а не на /css/main.css
Но оба этих определения равны, потому что Symfony приписывает к относительному пути
/css/
.
В случае, если у нас определены несколько файлов, Symfony включит их в том порядке, в каком они были определены:
stylesheets: [main.css, jobs.css, job.css]
Вы также можете изменить атрибут media
и поставить расширение отличное от .css
:
stylesheets: [main.css, jobs.css, job.css, print: { media: print }]
Данная конфигурация будет отображена как:
<link rel="stylesheet" type="text/css" media="screen" href="/css/main.css" /> <link rel="stylesheet" type="text/css" media="screen" href="/css/jobs.css" /> <link rel="stylesheet" type="text/css" media="screen" href="/css/job.css" /> <link rel="stylesheet" type="text/css" media="print" href="/css/print.css" />
tip
файл конфигурации view.yml
так же определяет стандартный шаблон, который
будет использоваться приложением. Изначально его имя layout
и Symfony обрамляет
каждую страницу файлом layout.php
. Вы можете вообще отменить процесс декорирования
изменив опцию has_layout
на false
.
Всё что нам сейчас нужно, это чтобы файл jobs.css
был прикручен к главной странице,
а job.css
к странице с вакансиями. Файл view.yml
может быть изменён
отдельно для каждого модуля. Измените view.yml
так, чтобы он содержал лишь файл
main.css
:
# apps/frontend/config/view.yml stylesheets: [main.css]
Чтобы изменить вид модуля job
, создайте файл view.yml
в каталоге
apps/frontend/modules/job/config/
:
# apps/frontend/modules/job/config/view.yml indexSuccess: stylesheets: [jobs.css] showSuccess: stylesheets: [job.css]
Под частями indexSuccess
и showSuccess
(это имена шаблонов,
который связаны с действиями index
и show
, о чём мы узнаем позже),
Вы можете изменять любую из опций, находящихся в default
в файле view.yml
,
относящемуся к приложению. Все данные опции будут частью конфигурации
приложения. Вы так же можете определить конфигурациию для всех действий модуля
при помощи раздела all
.
Правило большого пальца: если что-то где можно настроить через
файлы конфигурации, то же самое можно сделать и через код PHP.
Вместо создания view.yml
для модуля job
можно использовать
помощник use_stylesheet()
для вставки стиля в шаблон:
<?php use_stylesheet('main.css') ?>
Вы также можете использовать этот помощник в шаблоне, для вставки общих стилей.
Использовать ли один способ, или другой - дело вкуса. Файл view.yml
предоставляет способ определения параметров для всех действий модуля,
что невозможно в отдельном шаблоне, но конфигурация абсолютно статична.
С другой стороны, использование помощника use_stylesheet()
более гибкое
и помимо этого всё в одном месте: и стили, и HTML код. В Jobeet мы будет применять
помощники use_stylesheet()
, так что Вы можете удалить view.yml
, который мы только
что создали, и обновить шаблоны модуля job
, внеся туда вызовы use_stylesheet()
.
note
Пареллельно этому, конфигурация JavaScript осуществляется через опцию
javascripts
в файле view.yml
и через помощник use_javascript()
для
определения JavaScript файлов в шаблоне.
Главная страница вакансий
Как мы уже видели в третьем дне, главная страница вакансий генерируется
действием index
модуля job
. Действие index
это часть контроллера страницы
и оно связано с шаблоном indexSuccess.php
, который является представлением:
apps/ frontend/ modules/ job/ actions/ actions.class.php templates/ indexSuccess.php
Действие
Каждое действие определено методом класса. Для главной страницы вакансий
есть класс jobActions
(к имени модуля прибавляется Actions
), а так же
метод executeIndex()
(execute
прибавляется к имени действия).
Данное действие получает все вакансии из БД:
// apps/frontend/modules/job/actions/actions.class.php class jobActions extends sfActions { public function executeIndex(sfWebRequest $request) { $this->jobeet_job_list = JobeetJobPeer::doSelect(new Criteria()); } // ... }
Давайте взглянем поближе на код: метод executeIndex()
(контроллер)
вызывает модель JobeetJobPeer
для получения всех вакансий (new Criteria()
).
Последняя возвращает массив объектов JobeetJob
, которые привязаны к
к свойству объекта jobeet_job_list
.
Все подобные свойства объекта в дальнейшем передаются шаблону (представление). Для того чтобы передать данные из контроллера в представление, просто создайте ещё одно свойство:
public function executeIndex(sfWebRequest $request) { $this->foo = 'bar'; $this->bar = array('bar', 'baz'); }
Данный код сделает переменные $foo
и $bar
доступными для шаблона.
Шаблон
Изначально, названия шаблонов, связанных с действиями, определяются Symfony
благодаря правилам (к действию прибавляется Success
).
Шаблон indexSuccess.php
создаёт HTML таблицу для вывода всех вакансий:
<!-- apps/frontend/modules/job/templates/indexSuccess.php --> <h1>Job List</h1> <table> <thead> <tr> <th>Id</th> <th>Category</th> <th>Type</th> <!-- more columns here --> <th>Created at</th> <th>Updated at</th> </tr> </thead> <tbody> <?php foreach ($jobeet_job_list as $jobeet_job): ?> <tr> <td> <a href="<?php echo url_for('job/show?id='.$jobeet_job->getId()) ?>"> <?php echo $jobeet_job->getId() ?> </a> </td> <td><?php echo $jobeet_job->getCategoryId() ?></td> <td><?php echo $jobeet_job->getType() ?></td> <!-- more columns here --> <td><?php echo $jobeet_job->getCreatedAt() ?></td> <td><?php echo $jobeet_job->getUpdatedAt() ?></td> </tr> <?php endforeach; ?> </tbody> </table> <a href="<?php echo url_for('job/new') ?>">New</a>
В коде шаблона, foreach
пробегается по всем объектам Job
($jobeet_job_list
)
и выводит для каждой вакансии каждое значения поля.
Помните, получить доступ к значению поля просто - через вызов метода доступа,
имя которого начинается с get
и camelCased имени поля (например getCreatedAt()
для поля created_at
).
Давайте немного приберёмся в коде и организуем вывод лишь значений определённого набора доступных полей:
<!-- apps/frontend/modules/job/templates/indexSuccess.php --> <?php use_stylesheet('jobs.css') ?> <div id="jobs"> <table class="jobs"> <?php foreach ($jobeet_job_list as $i => $job): ?> <tr class="<?php echo fmod($i, 2) ? 'even' : 'odd' ?>"> <td><?php echo $job->getLocation() ?></td> <td> <a href="<?php echo url_for('job/show?id='.$job->getId()) ?>"> <?php echo $job->getPosition() ?> </a> </td> <td><?php echo $job->getCompany() ?></td> </tr> <?php endforeach; ?> </table> </div>
Функция url_for()
это очередной помощник, который мы обсудим завтра.
Шаблон отображения вакансии
А теперь изменим шаблон страницы с вакансией. Откройте файл showSuccess.php
и замените его содержимое следующим:
<?php use_stylesheet('job.css') ?> <?php use_helper('Text') ?> <div id="job"> <h1><?php echo $job->getCompany() ?></h1> <h2><?php echo $job->getLocation() ?></h2> <h3> <?php echo $job->getPosition() ?> <small> - <?php echo $job->getType() ?></small> </h3> <?php if ($job->getLogo()): ?> <div class="logo"> <a href="<?php echo $job->getUrl() ?>"> <img src="<?php echo $job->getLogo() ?>" alt="<?php echo $job->getCompany() ?> logo" /> </a> </div> <?php endif; ?> <div class="description"> <?php echo simple_format_text($job->getDescription()) ?> </div> <h4>How to apply?</h4> <p class="how_to_apply"><?php echo $job->getHowToApply() ?></p> <div class="meta"> <small>posted on <?php echo $job->getCreatedAt('m/d/Y') ?></small> </div> <div style="padding: 20px 0"> <a href="<?php echo url_for('job/edit?id='.$job->getId()) ?>">Edit</a> </div> </div>
Данный шаблон использует переменную $job
, которая передаётся действием, для
отображения информации о вакансии. Так как мы переименовали переменную,
передаваемую шаблону с $jobeet_job
на $job
, тоже самое нам нужно сделать в
дейстиии show
(будьте бдительны, эта переменная попадается нам дважды):
// apps/frontend/modules/job/actions/actions.class.php public function executeShow(sfWebRequest $request) { $this->job = JobeetJobPeer::retrieveByPk($request->getParameter('id')); $this->forward404Unless($this->job); }
Смею заметить, что методы доступа Propel принимают аргументы. Мы определили поле
created_at
как timestamp, поэтому метод getCreatedAt()
принимает
шаблон формата даты как первый аргумент:
$job->getCreatedAt('m/d/Y');
note
Описание вакансии использует помощник simple_format_text()
для форматирования
HTML, заменяя переносы строка на <br />
, например. Так как данный помощник
относится к группе помощников Text
, который изначально не загружается, то нам
придётся загрузить его самим при помощи помощника use_helper()
.
Слоты
На данный момент все заголовки на всех страницах в теге <title>
шаблона
layout выглядят так:
<title>Jobeet - Your best job board</title>
Но на странице вакансии мы бы хотели вывести куда более полезную информацию, включая имя компании и должность.
В Symfony, когда зона шаблона layout зависит от отображаемого шаблона, то мы должны определить слот:
Добавьте слот в шаблон, чтобы сделать заголовок более динамичным:
// apps/frontend/templates/layout.php <title><?php include_slot('title') ?></title>
Каждый слот определяется именем (title
) и может быть отображен при помощи
помощника include_slot()
. Теперь в начале шаблона showSuccess.php
используем помощник slot()
, чтобы определить содержимое слота на странице
вакансии:
// apps/frontend/modules/job/templates/showSuccess.php <?php slot('title', sprintf('%s is looking for a %s', $job->getCompany(), $job->getPosition())) ?>
Если заголовок сложен в генерации, то помощник slot()
можно использовать в виде блока:
// apps/frontend/modules/job/templates/showSuccess.php <?php slot('title') ?> <?php echo sprintf('%s is looking for a %s', $job->getCompany(), $job->getPosition()) ?> <?php end_slot(); ?>
На некоторых страницах, такие как главная, нужен обычный заголовок. Вместо повторения одного и того же заголовка вновь и вновь в шаблонах, мы можем определить стандартный заголовок в шаблоне:
// apps/frontend/templates/layout.php <title> <?php if (!include_slot('title')): ?> Jobeet - Your best job board <?php endif; ?> </title>
Помощник include_slot()
возвращает true
, если слот был определён. Так что,
когда Вы определяете содержимое слота title
в шаблоне, то используется оно;
если нет, что используется заголовок по умолчанию.
tip
Мы уже познакомились с несколькими помощниками, начинающимися с include_
.
Эти помощники выводят HTML и в большинстве случаев имеются сопряжённые помощники get_
, которые
просто напросто возвращают контент.
<?php include_slot('title') ?> <?php echo get_slot('title') ?> <?php include_stylesheets() ?> <?php echo get_stylesheets() ?>
Страница вакансии
Страница вакансии генерируется действием show
, определённым в методе
executeShow()
модуля job
:
class jobActions extends sfActions { public function executeShow(sfWebRequest $request) { $this->job = JobeetJobPeer::retrieveByPk($request->getParameter('id')); $this->forward404Unless($this->job); } // ... }
В действии index
класс JobeetJobPeer
используется для получения вакансии,
в данном случае через метод retrieveByPk()
. Параметром данного метода
является первичный ключ вакансии. Далее мы
объясним почему выражение $request->getParameter('id')
возвращает этот ключ.
tip
Сгенерированные классы моделей содержат много полезных методов для
взаимодействия с объектами проекта. Найдите время для просмотра кода,
расположенного в каталоге lib/om/
и раскройте всю силу, встроенную
в эти классы.
Если вакансия не существует в БД, то стоит перенаправить пользователя на страницу 404,
чем и занимается метод forward404Unless()
. Он принимает Boolean в виде
первого параметра и, до тех пор пока не он не будет true, останавливает
исполнение текущего процесса. Методы перенаправления останавливают
исполнение скрипта, выкидывая исключение sfError404Exception
, поэтому
не нужно в последствии ничего возвращать.
В случае исключения страница будет отображаться по разному в окружениях prod
и dev
:
note
Прежде чем мы развернем сайт Jobeet на рабочий сервер, Вам придётся научиться изменять основную страницу 404.
Запрос и ответ
Когда Вы попытаетесь запустить в браузере /job
или job/show/id/1/
, то
Вы запускаете кругосветное путешествие по веб-серверу. Браузер посылает
запрос, а сервер посылает обратно ответ.
Мы уже видели, что Symfony инкапсулирует запрос в объект sfWebRequest
(смотрите подпись к методу executeShow()
). И в Symfony, как в объекто-ориентированном фрейморке,
ответ также является объектом класса sfWebResponse
. Вы можете получить
доступ к объекту ответа в действии, вызвав $this->getResponse()
.
Данный объект предоставляет множество удобных методов, дающих доступ к информации из PHP-функций и глобальных переменных PHP.
note
Почему Symfony оборачивает функционал PHP своими классами? Во-первых, потому что методы Symfony более
мощные, чем их PHP аналоги. Затем, потому что, когда Вы тестируете приложение,
куда проще эмулировать объекты запроса или ответа, чем пытаться бегать вокруг
глобальных переменных или работать с PHP функциями, вроде header()
, которые
оставляют кучу магии нераскрытой.
The Request
Класс sfWebRequest
собирает глобальные массивы $_SERVER
, $_COOKIE
, $_GET
, $_POST
,
и $_FILES
PHP:
Method name | PHP equivalent |
---|---|
getMethod() |
$_SERVER['REQUEST_METHOD'] |
getUri() |
$_SERVER['REQUEST_URI'] |
getReferer() |
$_SERVER['HTTP_REFERER'] |
getHost() |
$_SERVER['HTTP_HOST'] |
getLanguages() |
$_SERVER['HTTP_ACCEPT_LANGUAGE'] |
getCharsets() |
$_SERVER['HTTP_ACCEPT_CHARSET'] |
isXmlHttpRequest() |
$_SERVER['X_REQUESTED_WITH'] == 'XMLHttpRequest' |
getHttpHeader() |
$_SERVER |
getCookie() |
$_COOKIE |
isSecure() |
$_SERVER['HTTPS'] |
getFiles() |
$_FILES |
getGetParameter() |
$_GET |
getPostParameter() |
$_POST |
getUrlParameter() |
$_SERVER['PATH_INFO'] |
getRemoteAddress() |
$_SERVER['REMOTE_ADDR'] |
Мы уже получали доступ к параметрам запроса через метод getParameter()
.
Она нам вернул значение из глобальных массивов $_GET
или $_POST
или же из переменной PATH_INFO
.
Если Вы хотите быть уверены в том, что параметр запроса приходит из
определённой переменной, то стоит воспользовать методами getGetParameter()
,
getPostParameter()
и getUrlParameter()
.
note
Когда Вы хотите запретить действию определённый метод, например когда Вы
хотите быть уверены в том, что форма пришла методом POST
, то
Вы можете использовать метод isMethod()
:
$this->forwardUnless($request->isMethod('POST'));
.
Ответ
Класс sfWebResponse
вбирает в себя методы header()
и setrawcookie()
:
Method name | PHP equivalent |
---|---|
setCookie() |
setrawcookie() |
setStatusCode() |
header() |
setHttpHeader() |
header() |
setContentType() |
header() |
addVaryHttpHeader() |
header() |
addCacheControlHttpHeader() |
header() |
Конечно же класс sfWebResponse
так же предоставляет возможность устанавливать
контент в ответе (setContent()
) и отправлять ответ браузеру (send()
).
Сегодня, чуть ранее, мы видели как можно управлять файлами стилей и JavaScript
в обоих представлениях view.yml
и в шаблонах. В конце концов, обе техники
отвечают методам addStylesheet()
и addJavascript()
объекта ответа.
tip
Классы sfAction
,
sfRequest
и
sfResponse
имеют
ещё много замечательных методов. Не стесняйтесь и взгляните на них в
документации API чтобы
знать больше про встроенные в Symfony классы.
Увидимся завтра!
Сегодя мы описали некоторые шаблоны проектирования, используемые в Symfony. Надеюсь теперь структура проекта куда более понятна. Мы поигрались с шаблонами, изменяя layout, и файлами шаблонов. Мы так же сделали их чуть более динамичными, благодаря слотам и действиям.
Завтра мы узнаем больше о помощнике url_for()
, который сегодня использовали,
и о маршрутизации, связанной с ним.
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.