diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..0390455 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: terminal42 +ko_fi: terminal42 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..44bf94b --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,81 @@ +name: CI + +on: + pull_request: ~ + push: + branches: + - '*' + tags: + - '*' + schedule: + - cron: 0 13 * * MON + +jobs: + cs: + name: Coding Style + runs-on: ubuntu-latest + if: github.event_name != 'push' + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + + - name: Checkout + uses: actions/checkout@v2 + + - name: Install ecs + run: cd tools/ecs && composer install --no-interaction --no-suggest + + - name: Run the CS fixer + run: composer cs-fixer + + tests: + name: PHP ${{ matrix.php }} + runs-on: ubuntu-latest + if: github.event_name != 'push' + strategy: + fail-fast: false + matrix: + php: [8.1, 8.2] + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + + - name: Checkout + uses: actions/checkout@v2 + + - name: Install phpunit + run: cd tools/phpunit && composer install --no-interaction --no-suggest + + - name: Install the dependencies + run: composer install --no-interaction --no-suggest + + - name: Run the unit tests + run: composer unit-tests + + prefer-lowest-tests: + name: PHP ${{ matrix.php }} --prefer-lowest + runs-on: ubuntu-latest + if: github.event_name != 'push' + strategy: + fail-fast: false + matrix: + php: [8.1, 8.2] + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + + - name: Checkout + uses: actions/checkout@v2 + + - name: Install phpunit + run: cd tools/phpunit && composer install --no-interaction --no-suggest + + - name: Install the dependencies + run: composer update --prefer-lowest --prefer-stable --no-interaction --no-suggest + + - name: Run the unit tests + run: composer unit-tests diff --git a/.gitignore b/.gitignore index 5247b18..959d6d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,5 @@ -# OS -.DS_Store -Thumbs.db - -# IDEs -.buildpath -.project -.settings/ -.build/ -.idea/ -nbproject/ - -# Composer -vendor/ -composer.lock +/vendor/ +/tools/*/vendor +/composer.lock +/.php-cs-fixer.cache +/tools/phpunit/.phpunit.result.cache diff --git a/FormMPFormPageSwitch.php b/FormMPFormPageSwitch.php deleted file mode 100644 index 2cd25bc..0000000 --- a/FormMPFormPageSwitch.php +++ /dev/null @@ -1,80 +0,0 @@ - - * @license http://opensource.org/licenses/lgpl-3.0.html LGPL - * @link https://github.com/terminal42/contao-mp_forms - */ - -use Contao\Widget; - -class FormMPFormPageSwitch extends Widget -{ - /** - * Template - * - * @var string - */ - protected $strTemplate = 'form_mp_form_page_switch'; - - /** - * The CSS class prefix - * - * @var string - */ - protected $strPrefix = 'widget widget-pagebreak'; - - /** - * Submit indicator - * @var boolean - */ - protected $blnSubmitInput = true; - - /** - * Do not validate this form field - * - * @param string - * - * @return string - */ - public function validator($input) - { - return $input; - } - - /** - * Add custom HTML after the widget - * - * @param array $attributes - * - * @return string - */ - public function parse($attributes = null) - { - if (TL_MODE == 'BE') { - $template = new BackendTemplate('be_wildcard'); - $template->wildcard = '### PAGE BREAK ###'; - - return $template->parse(); - } - - $manager = new MPFormsFormManager($this->pid); - - $this->canGoBack = !$manager->isFirstStep(); - - return parent::parse($attributes); - } - - /** - * Old generate() method that must be implemented due to abstract declaration. - * - * @throws \BadMethodCallException - */ - public function generate() - { - throw new BadMethodCallException('Calling generate() has been deprecated, you must use parse() instead!'); - } -} diff --git a/MPForms.php b/MPForms.php deleted file mode 100644 index c57958b..0000000 --- a/MPForms.php +++ /dev/null @@ -1,225 +0,0 @@ - - * @license http://opensource.org/licenses/lgpl-3.0.html LGPL - * @link https://github.com/terminal42/contao-mp_forms - */ - -use Contao\Controller; -use Contao\Form; -use Contao\FormFieldModel; -use Contao\FormModel; -use Contao\Input; -use Contao\Widget; - -class MPForms -{ - /** - * Adjust form fields to given page. - * - * @param FormFieldModel[] $formFields - * @param string $formId - * @param Form $form - */ - public function compileFormFields($formFields, $formId, Form $form) - { - // Make sure empty form fields arrays are skipped - if (0 === count($formFields)) { - - return $formFields; - } - - $manager = new MPFormsFormManager($form->id); - - // Don't try to render multi page form if no valid combination - if (!$manager->isValidFormFieldCombination()) { - - return $manager->getFieldsWithoutPageBreaks(); - } - - // If there is form data submitted in this step, store the original values here no matter if we're going back or if we continue. - if ($_POST) { - $manager->setPostData($_POST); - } - - // Do not let Contao validate anything if user wants to go back - // but still save data already added to the input fields so it is - // there when they come back to the current step - if ('back' === ($_POST['mp_form_pageswitch'] ?? null)) { - $manager->storeData($_POST, [], (array) ($_SESSION['FILES'] ?? [])); - $this->redirectToStep($manager, $manager->getPreviousStep()); - } - - // Validate previous form data if we're not on the first step. This has to be done - // no matter if we're in a POST request right now or not as otherwise you can submit - // a POST request without any previous step data (e.g. by deleting the session cookie - // manually) - if (!$manager->isFirstStep()) { - $vResult = $manager->validateSteps(0, $manager->getCurrentStep() - 1); - if (true !== $vResult) { - $manager->setPreviousStepsWereInvalid(); - $this->redirectToStep($manager, $vResult); - } - } - - // If someone wanted to skip the page, fake form submission so fields - // are validated and show the error message. - if ($manager->getPreviousStepsWereInvalid()) { - Input::setPost('FORM_SUBMIT', $manager->getFormId()); - $manager->resetPreviousStepsWereInvalid(); - } - - return $manager->getFieldsForStep($manager->getCurrentStep()); - } - - /** - * Loads the values from the session and adds it as default value to the - * widget. - * - * @param Widget $widget - * @param string $formId - * @param array $formData - * @param Form $form - * - * @return Widget - */ - public function loadValuesFromSession(Widget $widget, $formId, $formData, Form $form) - { - $manager = new MPFormsFormManager($form->id); - - if ($manager->isStoredInData($widget->name)) { - $widget->value = $manager->fetchFromData($widget->name); - } - - return $widget; - } - - /** - * Store the submitted data into the session and redirect to the next step - * unless it's the last. - * - * @param array $submitted - * @param array $labels - * @param $fieldsOrForm - * @param $formOrFields - */ - public function prepareFormData(&$submitted, &$labels, $fieldsOrForm, $formOrFields) - { - // Compat with Contao 4 and 3.5 - $form = $fieldsOrForm instanceof Form ? $fieldsOrForm : $formOrFields; - - $manager = new MPFormsFormManager($form->id); - - // Don't do anything if not valid - if (!$manager->isValidFormFieldCombination()) { - - return; - } - - $pageSwitchValue = $submitted['mp_form_pageswitch']; - unset($submitted['mp_form_pageswitch']); - - // Store data in session - $manager->storeData($submitted, $labels, (array) ($_SESSION['FILES'] ?? [])); - - // Submit form - if ($manager->isLastStep() && 'continue' === $pageSwitchValue) { - - $allData = $manager->getDataOfAllSteps(); - - // Replace data by reference and then return so the default Contao - // routine kicks in - $submitted = $allData['submitted']; - $labels = $allData['labels']; - $_SESSION['FILES'] = $allData['files']; - - // Override $_POST so Contao handles special cases like "email" - // too if the data was submitted in a previous step - $_POST = $submitted; - - // Override $_SESSION['FORM_DATA'] so it contains the data of - // previous steps as well - $_SESSION['FORM_DATA'] = $submitted; - - // Clear session - $manager->resetData(); - return; - } else { - // Make sure the Contao form data session handling doesn't do - // anything at all while we're on a multipage form - $_SESSION['FORM_DATA'] = []; - } - - $this->redirectToStep($manager, $manager->getNextStep()); - } - - /** - * Replace InsertTags. - * - * @param string $tag - * - * @return int|false - */ - public function replaceTags($tag) - { - if (strpos($tag, 'mp_forms::') === false) { - - return false; - } - - $chunks = explode('::', $tag); - $formId = $chunks[1]; - $type = $chunks[2]; - $value = $chunks[3] ?? ''; - - $form = FormModel::findByPk($formId); - $manager = new MPFormsFormManager($form->id); - - // BC - if (\in_array($type, ['current', 'total', 'percentage', 'numbers'], true)) { - $value = $type; - $type = 'step'; - } - - switch ($type) { - case 'step': - return $this->getStepValue($manager, $value); - case 'field_value': - $allData = $manager->getDataOfAllSteps(); - return $allData['submitted'][$value] ?? ''; - } - - return ''; - } - - private function getStepValue(MPFormsFormManager $manager, $value) - { - switch ($value) { - case 'current': - return (int) $manager->getCurrentStep() + 1; - case 'total': - return $manager->getNumberOfSteps(); - case 'percentage': - return ($manager->getCurrentStep() + 1) / ($manager->getNumberOfSteps()) * 100; - case 'numbers': - return ($manager->getCurrentStep() + 1) . ' / ' . ($manager->getNumberOfSteps()); - } - - return ''; - } - - /** - * Redirect to step. - * - * @param MPFormsFormManager $manager - * @param int $step - */ - private function redirectToStep(MPFormsFormManager $manager, $step) - { - Controller::redirect($manager->getUrlForStep($step)); - } -} diff --git a/MPFormsFormManager.php b/MPFormsFormManager.php deleted file mode 100644 index abfaa54..0000000 --- a/MPFormsFormManager.php +++ /dev/null @@ -1,686 +0,0 @@ - - * @license http://opensource.org/licenses/lgpl-3.0.html LGPL - * @link https://github.com/terminal42/contao-mp_forms - */ - -use Contao\Form; -use Contao\FormCaptcha; -use Contao\FormFieldModel; -use Contao\FormModel; -use Contao\Input; -use Contao\System; -use Contao\Widget; -use Haste\Util\Url; -use Symfony\Component\HttpFoundation\Request; - -class MPFormsFormManager -{ - public const SESSION_KEY = 'contao.mp_forms'; - - /** - * @var FormModel - */ - private $formModel; - - /** - * @var Request - */ - private $request; - - /** - * @var MPFormsSessionManager - */ - private $sessionManager; - - /** - * @var FormFieldModel[] - */ - private $formFieldModels; - - /** - * Array containing the fields per step - * - * @var array - */ - private $formFieldsPerStep = []; - - /** - * True if the manager can handle this form - * - * @var bool - */ - private $isValidFormFieldCombination = true; - - /** - * @param int $formGeneratorId - */ - public function __construct($formGeneratorId) - { - $this->formModel = FormModel::findByPk($formGeneratorId); - $this->request = System::getContainer()->get('request_stack')->getCurrentRequest(); - $this->sessionManager = new MPFormsSessionManager($formGeneratorId); - - $this->prepareFormFields(); - } - - /** - * Checks if the combination is valid. - * - * @return bool - */ - public function isValidFormFieldCombination() - { - return $this->isValidFormFieldCombination - && $this->getNumberOfSteps() > 1; - } - - /** - * Gets the GET param for the steps. - * - * @return string - */ - public function getGetParam() - { - return $this->sessionManager->getGetParam(); - } - - /** - * Gets the GET param for the session reference. - * - * @return string - */ - public function getGetParamForSessionReference() - { - return $this->sessionManager->getGetParamForSessionReference(); - } - - /** - * Gets the form generator form id. - * - * @return string - */ - public function getFormId() - { - return ('' !== $this->formModel->formID) ? - 'auto_' . $this->formModel->formID : - 'auto_form_' . $this->formModel->id; - } - - /** - * Get the number of steps of the form - * - * @return int number of steps - */ - public function getNumberOfSteps() - { - return count(array_keys($this->formFieldsPerStep)); - } - - /** - * Check if a given step is available - * - * @param int $step - * - * @return boolean - */ - public function hasStep($step = 0) - { - return isset($this->formFieldsPerStep[$step]); - } - - /** - * Get the fields for a given step. - * - * @param int $step - * - * @return FormFieldModel[] - * - * @throws InvalidArgumentException - */ - public function getFieldsForStep($step = 0) - { - if (!$this->hasStep($step)) { - throw new InvalidArgumentException('Step "' . $step . '" is not available!'); - } - - return $this->formFieldsPerStep[$step]; - } - - /** - * Get the fields without the page breaks. - * - * @return FormFieldModel[] - */ - public function getFieldsWithoutPageBreaks() - { - $formFields = $this->formFieldModels; - - foreach ($formFields as $k => $formField) { - if ('mp_form_pageswitch' === $formField->type) { - unset($formFields[$k]); - } - } - - return $formFields; - } - - /** - * Gets the label for a given step. - * - * @param int $step - * - * @return string - */ - public function getLabelForStep($step) - { - foreach ($this->getFieldsForStep($step) as $formField) { - if ($this->isPageBreak($formField) && '' !== $formField->label) { - - return $formField->label; - } - } - - return 'Step ' . ($step + 1); - } - - /** - * Gets the url fragment for a given step - * - * @param int $step - * @param string $mode ("next" or "back") - * - * @return string - */ - public function getFragmentForStep($step, $mode) - { - if (!\in_array($mode, ['back', 'next'], true)) { - throw new \InvalidArgumentException('Mode must be either "back" or "next".'); - } - - $key = sprintf('mp_forms_%sFragment', $mode); - - foreach ($this->getFieldsForStep($step) as $formField) { - if ($this->isPageBreak($formField) && '' !== $formField->{$key}) { - - return $formField->{$key}; - } - } - - if ('' !== $this->formModel->{$key}) { - return $this->formModel->{$key}; - } - - return ''; - } - - /** - * Gets the current step. - * - * @return int - */ - public function getCurrentStep() - { - return $this->request->query->getInt($this->getGetParam()); - } - - /** - * Gets the previous step. - * - * @return int - */ - public function getPreviousStep() - { - $previous = $this->getCurrentStep() - 1; - - if ($previous < 0) { - $previous = 0; - } - - return $previous; - } - - /** - * Gets the next step. - * - * @return int - */ - public function getNextStep() - { - $next = $this->getCurrentStep() + 1; - - if ($next > $this->getNumberOfSteps()) { - $next = $this->getNumberOfSteps(); - } - - return $next; - } - - /** - * Check if current step is the first. - * - * @return bool - */ - public function isFirstStep() - { - if (0 === $this->getCurrentStep()) { - - return true; - } - - return false; - } - - /** - * Check if current step is the last. - * - * @return bool - */ - public function isLastStep() - { - if ($this->getCurrentStep() >= ($this->getNumberOfSteps() - 1)) { - - return true; - } - - return false; - } - - /** - * Generates an url for the step. - * - * @param int $step - * - * @return string - */ - public function getUrlForStep($step) - { - if (0 === $step) { - $url = Url::removeQueryString([$this->getGetParam()]); - } else { - $url = Url::addQueryString($this->getGetParam() . '=' . $step); - } - - $url = Url::addQueryString($this->sessionManager->getGetParamForSessionReference() . '=' . $this->sessionManager->getSessionRef(), $url); - - if ($step > $this->getCurrentStep()) { - $fragment = $this->getFragmentForStep($step, 'next'); - } else { - $fragment = $this->getFragmentForStep($this->getCurrentStep(), 'back'); - } - - if ($fragment) { - $url .= '#' . $fragment; - } - - return $url; - } - - public function setPostData(array $postData): self - { - $this->sessionManager->setPostData($postData); - - return $this; - } - - /** - * Store data. - * - * @param array $submitted - * @param array $labels - * @param array $files - */ - public function storeData(array $submitted, array $labels, array $files): void - { - $this->sessionManager->storeData($submitted, $labels, $files); - } - - /** - * Get data of given step. - * - * @param int $step - * - * @return array - */ - public function getDataOfStep($step) - { - return $this->sessionManager->getDataOfStep($step); - } - - /** - * Get data of all steps merged into one array. - */ - public function getDataOfAllSteps(): array - { - return $this->sessionManager->getDataOfAllSteps(); - } - - public function resetData(): void - { - $this->sessionManager->resetData(); - } - - /** - * Validates all steps, optionally accepting custom from -> to ranges - * to validate only a subset of steps. - * - * @param null $stepFrom - * @param null $stepTo - * - * @return true|int True if all steps valid, otherwise the step that failed - * validation - */ - public function validateSteps($stepFrom = null, $stepTo = null) - { - if (null === $stepFrom) { - $stepFrom = 0; - } - - if (null === $stepTo) { - $stepTo = $this->getNumberOfSteps() - 1; - } - - $steps = range($stepFrom, $stepTo); - foreach ($steps as $step) { - if (false === $this->validateStep($step)) { - - return $step; - } - } - - return true; - } - - /** - * Validates a step. - * - * @param $step - * - * @return bool - */ - public function validateStep($step) - { - $formFields = $this->getFieldsForStep($step); - - foreach ($formFields as $formField) { - if (false === $this->validateField($formField, $step)) { - - return false; - } - } - - return true; - } - - /** - * Validates a field. - * - * @param FormFieldModel $formField - * @param int $step - * - * @return bool - */ - public function validateField(FormFieldModel $formField, $step) - { - $class = $GLOBALS['TL_FFL'][$formField->type]; - - if (!class_exists($class)) { - return true; - } - - /** @var Widget $widget */ - $widget = new $class($formField->row()); - $widget->required = $formField->mandatory ? true : false; - $widget->decodeEntities = true; // Always decode entities - - // Needed for the hook - $form = $this->createDummyForm(); - - // HOOK: load form field callback - if (isset($GLOBALS['TL_HOOKS']['loadFormField']) && is_array($GLOBALS['TL_HOOKS']['loadFormField'])) { - foreach ($GLOBALS['TL_HOOKS']['loadFormField'] as $callback) { - $objCallback = System::importStatic($callback[0]); - $widget = $objCallback->{$callback[1]}($widget, $this->getFormId(), $this->formModel->row(), $form); - } - } - - // Validation (needs to set POST values because the widget class searches - // only in POST values :-( - // This should only happen if value is not currently submitted and if - // the value is neither submitted in POST nor in the session, we have - // to default it to an empty string so the widget validates for mandatory - // fields - $fakeValidation = false; - - if (!$this->checkWidgetSubmittedInCurrentStep($widget)) { - - // Handle regular fields - if ($this->isStoredInData($widget->name, $step)) { - $value = $this->fetchFromData($widget->name, $step); - - if ($widget->useRawRequestData) { - $this->request->request->set($widget->name, $value); - - // Special handling for FormPassword (must have already been correct once so we can reuse the submitted value) - if ($widget instanceof \Contao\FormPassword) { - $this->request->request->set($widget->name . '_confirm', $value); - } - } else { - Input::setPost($widget->name, $value); - } - } else { - Input::setPost($widget->name, ''); - } - - // Handle files - if ($this->isStoredInData($widget->name, $step, 'files')) { - $_FILES[$widget->name] = $this->fetchFromData($widget->name, $step, 'files'); - } - - $fakeValidation = true; - } - - $widget->validate(); - - // HOOK: validate form field callback - if (isset($GLOBALS['TL_HOOKS']['validateFormField']) && is_array($GLOBALS['TL_HOOKS']['validateFormField'])) { - foreach ($GLOBALS['TL_HOOKS']['validateFormField'] as $callback) { - - // Do not call ourselves recursively - if ('MPForms' === $callback[0]) { - continue; - } - - $objCallback = System::importStatic($callback[0]); - $widget = $objCallback->{$callback[1]}($widget, $this->getFormId(), $this->formModel->row(), $form); - } - } - - // Reset fake validation - if ($fakeValidation) { - Input::setPost($formField->name, null); - } - - // Special hack for upload fields because they delete $_FILES and thus - // multiple validation calls will fail - sigh - if ($widget instanceof \uploadable && isset($_SESSION['FILES'][$widget->name])) { - $_FILES[$widget->name] = $_SESSION['FILES'][$widget->name]; - } - - return !$widget->hasErrors(); - } - - /** - * Stores if some previous step was invalid into the session. - */ - public function setPreviousStepsWereInvalid() - { - $this->sessionManager->setPreviousStepsWereInvalid(); - } - - /** - * Checks if some previous step was invalid from the session. - * - * @return bool - */ - public function getPreviousStepsWereInvalid() - { - return $this->sessionManager->getPreviousStepsWereInvalid(); - } - - /** - * Resets the session for the previous step check. - */ - public function resetPreviousStepsWereInvalid() - { - $this->sessionManager->resetPreviousStepsWereInvalid(); - } - - /** - * Check if there is data stored for a certain field name. - * - * @param $fieldName - * @param null|int $step Current step if null - * @param string $key - * - * @return bool - */ - public function isStoredInData($fieldName, $step = null, $key = 'submitted') - { - return $this->sessionManager->isStoredInData($fieldName, $step, $key); - } - - /** - * Retrieve the value stored for a certain field name. - * - * @param $fieldName - * @param null|int $step Current step if null - * @param string $key - * - * @return mixed - */ - public function fetchFromData($fieldName, $step = null, $key = 'originalPostData') - { - return $this->sessionManager->fetchFromData($fieldName, $step, $key); - } - - /** - * Helper to check whether a formfieldmodel is of type page break. - * - * @param FormFieldModel $formField - * - * @return bool - */ - public function isPageBreak(FormFieldModel $formField) - { - return 'mp_form_pageswitch' === $formField->type; - } - - /** - * Checks if a widget was submitted in current step handling some - * exceptions. - * - * @return bool - */ - private function checkWidgetSubmittedInCurrentStep(Widget $widget) - { - // Special handling for captcha field - if ($widget instanceof FormCaptcha) { - return isset($_POST[$captcha['captcha_' . $widget->id]]); - } - - return isset($_POST[$widget->name]); - } - - /** - * Prepare an array that splits up the fields into steps - */ - private function prepareFormFields() - { - if (null === $this->formModel) { - $this->isValidFormFieldCombination = false; - return; - } - - $this->loadFormFieldModels(); - - if (0 === count($this->formFieldModels)) { - $this->isValidFormFieldCombination = false; - return; - } - - $i = 0; - foreach ($this->formFieldModels as $formField) { - $this->formFieldsPerStep[$i][] = $formField; - - if ($this->isPageBreak($formField)) { - // Set the name on the model, otherwise one has to enter it - // in the back end every time - $formField->name = $formField->type; - - // Increase counter - $i++; - } - - // If we have a regular submit form field, that's a misconfiguration - if ('submit' === $formField->type) { - $this->isValidFormFieldCombination = false; - } - } - } - - /** - * Loads the form field models (calling the compileFormFields hook and ignoring itself). - */ - private function loadFormFieldModels() - { - $formFieldModels = FormFieldModel::findPublishedByPid($this->formModel->id); - - if (null === $formFieldModels) { - $formFieldModels = []; - } else { - $formFieldModels = $formFieldModels->getModels(); - } - - // Needed for the hook - $form = $this->createDummyForm(); - - if (isset($GLOBALS['TL_HOOKS']['compileFormFields']) && is_array($GLOBALS['TL_HOOKS']['compileFormFields'])) { - foreach ($GLOBALS['TL_HOOKS']['compileFormFields'] as $k => $callback) { - - // Do not call ourselves recursively - if ('MPForms' === $callback[0]) { - continue; - } - - $objCallback = System::importStatic($callback[0]); - $formFieldModels = $objCallback->{$callback[1]}($formFieldModels, $this->getFormId(), $form); - } - } - - $this->formFieldModels = $formFieldModels; - } - - /** - * Creates a dummy form instance that is needed for the hooks. - * - * @return Form - */ - private function createDummyForm() - { - $form = new stdClass(); - $form->form = $this->formModel->id; - - // Set properties to avoid a warning "Undefined property: stdClass::$variable" - $form->headline = null; - $form->typePrefix = null; - $form->cssID = null; - - return new Form($form); - } -} diff --git a/MPFormsSessionManager.php b/MPFormsSessionManager.php deleted file mode 100644 index 7a7e7bd..0000000 --- a/MPFormsSessionManager.php +++ /dev/null @@ -1,329 +0,0 @@ - - * @license http://opensource.org/licenses/lgpl-3.0.html LGPL - * @link https://github.com/terminal42/contao-mp_forms - */ - -use Contao\FormModel; -use Contao\System; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Session\SessionInterface; -use Symfony\Component\PropertyAccess\PropertyAccessorBuilder; - -class MPFormsSessionManager -{ - public const SESSION_KEY = 'contao.mp_forms'; - - /** - * @var FormModel - */ - private $formModel; - - /** - * @var Request - */ - private $request; - - /** - * @param int $formGeneratorId - */ - public function __construct($formGeneratorId) - { - $this->formModel = FormModel::findByPk($formGeneratorId); - $this->request = System::getContainer()->get('request_stack')->getCurrentRequest(); - } - - /** - * Gets the GET param for the steps. - * - * @return string - */ - public function getGetParam() - { - return $this->formModel->mp_forms_getParam ?: 'step'; - } - - /** - * Gets the GET param for the session reference. - * - * @return string - */ - public function getGetParamForSessionReference() - { - return $this->formModel->mp_forms_sessionRefParam ?: 'ref'; - } - - /** - * Gets the current step. - * - * @return int - */ - public function getCurrentStep() - { - return $this->request->query->getInt($this->getGetParam()); - } - - public function setPostData(array $postData): void - { - $this->writeToSession(sprintf('[MPFORMSTORAGE_POSTDATA][%s][%d]', - $this->getSessionIdentifier(), - $this->getCurrentStep() - ), $postData); - } - - /** - * Store data. - */ - public function storeData(array $submitted, array $labels, array $files): void - { - // Make sure files are moved to our own tmp directory so they are - // kept across php processes - foreach ($files as $k => $file) { - // If the user marked the form field to upload the file into - // a certain directory, this check will return false and thus - // we won't move anything. - - if (is_uploaded_file($file['tmp_name'])) { - $target = sprintf('%s/mp_forms_%s.%s', - sys_get_temp_dir(), - basename($file['tmp_name']), - $this->guessFileExtension($file) - ); - move_uploaded_file($file['tmp_name'], $target); - $files[$k]['tmp_name'] = $target; - - // Compatibility with notification center - $files[$k]['uploaded'] = true; - } - } - - // If the current step is 0, we don't want to check for hasPreviousSession(), as this is false on initial page - // load (in case there is no previous session of course) - $checkPreviousSessionForPostData = $this->getCurrentStep() !== 0; - - $this->writeToSession(sprintf('[MPFORMSTORAGE][%s][%d]', - $this->getSessionIdentifier(), - $this->getCurrentStep() - ), [ - 'submitted' => $submitted, - 'labels' => $labels, - 'files' => $files, - 'originalPostData' => $this->readFromSession(sprintf('[MPFORMSTORAGE_POSTDATA][%s][%d]', - $this->getSessionIdentifier(), - $this->getCurrentStep() - ), $checkPreviousSessionForPostData) ?? [], - ]); - } - - /** - * Get data of given step. - * - * @param int $step - * - * @return array - */ - public function getDataOfStep($step) - { - return (array) $this->readFromSession(sprintf('[MPFORMSTORAGE][%s][%d]', - $this->getSessionIdentifier(), - $step - )); - } - - /** - * Get data of all steps merged into one array. - */ - public function getDataOfAllSteps(): array - { - $submitted = []; - $labels = []; - $files = []; - $originalPostData = []; - - foreach ((array) $this->readFromSession(sprintf('[MPFORMSTORAGE][%s]', $this->getSessionIdentifier())) as $stepData) { - $submitted = array_merge($submitted, (array) $stepData['submitted']); - $labels = array_merge($labels, (array) $stepData['labels']); - $files = array_merge($files, (array) $stepData['files']); - $originalPostData = array_merge($files, (array) $stepData['originalPostData']); - } - - return [ - 'submitted' => $submitted, - 'labels' => $labels, - 'files' => $files, - 'originalPostData' => $originalPostData, - ]; - } - - public function resetData() - { - foreach (['MPFORMSTORAGE', 'MPFORMSTORAGE_POSTDATA', 'MPFORMSTORAGE_PSWI'] as $sessionKey) { - $data = $this->readFromSession(sprintf('[%s]', $sessionKey)); - - foreach (array_keys((array) $data) as $sessionIdentifier) { - if (0 === strncmp($sessionIdentifier, $this->formModel->id, \strlen($this->formModel->id))) { - $this->writeToSession(sprintf('[%s][%s]', $sessionKey, $sessionIdentifier), []); - } - } - } - } - - /** - * Stores if some previous step was invalid into the session. - */ - public function setPreviousStepsWereInvalid() - { - $this->writeToSession(sprintf('[MPFORMSTORAGE_PSWI][%s]', - $this->getSessionIdentifier() - ), true); - } - - /** - * Checks if some previous step was invalid from the session. - * - * @return bool - */ - public function getPreviousStepsWereInvalid() - { - return true === $this->readFromSession(sprintf('[MPFORMSTORAGE_PSWI][%s]', - $this->getSessionIdentifier() - )); - } - - /** - * Resets the session for the previous step check. - */ - public function resetPreviousStepsWereInvalid() - { - $this->writeToSession(sprintf('[MPFORMSTORAGE_PSWI][%s]', - $this->getSessionIdentifier() - ), []); - } - - /** - * Check if there is data stored for a certain field name. - * - * @param $fieldName - * @param null|int $step Current step if null - * @param string $key - * - * @return bool - */ - public function isStoredInData($fieldName, $step = null, $key = 'submitted') - { - $step = null === $step ? $this->getCurrentStep() : $step; - - return isset($this->getDataOfStep($step)[$key]) - && array_key_exists($fieldName, $this->getDataOfStep($step)[$key]); - } - - /** - * Retrieve the value stored for a certain field name. - * - * @param $fieldName - * @param null|int $step Current step if null - * @param string $key - * - * @return mixed - */ - public function fetchFromData($fieldName, $step = null, $key = 'originalPostData') - { - $step = null === $step ? $this->getCurrentStep() : $step; - - return $this->getDataOfStep($step)[$key][$fieldName] ?? null; - } - - /** - * @return string - */ - private function getSessionIdentifier() - { - return $this->formModel->id . '__' . $this->getSessionRef(); - } - - /** - * Cannot make this a class property because people use `new MPFormsFormManager()` all over the place. - * We can only introduce proper handling here once there's a completely new version of this extension - * using DI. - * - * @return string - */ - public function getSessionRef() - { - static $sessionRef; - - if (null !== $sessionRef) { - return $sessionRef; - } - - return $sessionRef = $this->request->query->get( - $this->getGetParamForSessionReference(), - bin2hex(random_bytes(16)) - ); - } - - private function guessFileExtension(array $file) - { - $extension = 'unknown'; - - if (!isset($file['type'])) { - return $extension; - } - - foreach ($GLOBALS['TL_MIME'] as $ext => $data) { - if ($data[0] === $file['type']) { - $extension = $ext; - break; - - } - } - - return $extension; - } - - private function writeToSession(string $propertyPath, $value): void - { - if (null === ($session = $this->getSession())) { - return; - } - - $data = $session->get(self::SESSION_KEY, []); - - $pa = (new PropertyAccessorBuilder())->getPropertyAccessor(); - - $pa->setValue($data, $propertyPath, $value); - - $session->set(self::SESSION_KEY, $data); - } - - private function readFromSession(string $propertyPath, bool $checkPrevious = false) - { - if (null === ($session = $this->getSession($checkPrevious))) { - return null; - } - - $data = $session->get(self::SESSION_KEY, []); - - $pa = (new PropertyAccessorBuilder())->getPropertyAccessor(); - - return $pa->getValue($data, $propertyPath); - } - - private function getSession(bool $checkPrevious = false): ?SessionInterface - { - if ($checkPrevious && !$this->request->hasPreviousSession()) { - return null; - } - - if (!$this->request->hasSession()) { - return null; - } - - return $this->request->getSession(); - } -} diff --git a/MPFormsStepsModule.php b/MPFormsStepsModule.php deleted file mode 100644 index 95b6646..0000000 --- a/MPFormsStepsModule.php +++ /dev/null @@ -1,103 +0,0 @@ - - * @license http://opensource.org/licenses/lgpl-3.0.html LGPL - * @link https://github.com/terminal42/contao-mp_forms - */ - -use Contao\Module; -use Contao\BackendTemplate; -use Contao\FrontendTemplate; -use Haste\Generator\RowClass; - -class MPFormsStepsModule extends Module -{ - - /** - * Template - * @var string - */ - protected $strTemplate = 'mod_mp_form_steps'; - - - /** - * Display a wildcard in the back end - * - * @return string - */ - public function generate() - { - if (TL_MODE == 'BE') { - /** @var BackendTemplate|object $objTemplate */ - $objTemplate = new BackendTemplate('be_wildcard'); - - $objTemplate->wildcard = '### ' . utf8_strtoupper($GLOBALS['TL_LANG']['FMD']['mp_form_steps'][0]) . ' ###'; - $objTemplate->title = $this->headline; - $objTemplate->id = $this->id; - $objTemplate->link = $this->name; - $objTemplate->href = 'contao/main.php?do=themes&table=tl_module&act=edit&id=' . $this->id; - - return $objTemplate->parse(); - } - - return parent::generate(); - } - - - /** - * Generate the module - */ - protected function compile() - { - $navTpl = new FrontendTemplate($this->navigationTpl ?: 'nav_default'); - $navTpl->level = 0; - $navTpl->items = $this->buildNavigationItems(); - $this->Template->navigation = $navTpl->parse(); - } - - /** - * Builds the navigation array items. - * - * @return array - */ - private function buildNavigationItems() - { - $manager = new MPFormsFormManager($this->form); - - $steps = range(0, $manager->getNumberOfSteps() - 1); - $items = []; - - // Never validate the very last step - $firstFailingStep = $manager->validateSteps(0, $manager->getNumberOfSteps() - 2); - - foreach ($steps as $step) { - - // Check if step can be accessed - $cantBeAccessed = true !== $firstFailingStep && $step > $firstFailingStep; - - // Only active if current step or step cannot be accessed because of - // previous steps - $isActive = $step === $manager->getCurrentStep() || $cantBeAccessed; - - $items[] = [ - 'isActive' => $isActive, - 'class' => 'step_' . $step . (($cantBeAccessed) ? ' forbidden' : ''), - 'href' => $manager->getUrlForStep($step), - 'title' => $manager->getLabelForStep($step), - 'link' => $manager->getLabelForStep($step), - 'nofollow' => true - ]; - } - - RowClass::withKey('class') - ->addFirstLast() - ->addEvenOdd() - ->applyTo($items); - - return $items; - } -} diff --git a/README.md b/README.md index 452f4b3..8c588d1 100644 --- a/README.md +++ b/README.md @@ -57,13 +57,14 @@ label field will be used for the navigation if you provide it. There are insert tags you can use to fetch information about the state of the form: -| Insert tag | Description | Example | -|---|---|---| -| `{{mp_forms::
::step::current}}` | Contains the current step you are on | 2 | -| `{{mp_forms::::step::total}}` | Contains the total steps of your form | 5 | -| `{{mp_forms::::step::percentage}}` | Contains the percentage of your progress | 20 | -| `{{mp_forms::::step::numbers}}` | Contains a classic `x of y` display | `2 / 5`| -| `{{mp_forms::::field_value::}}` | Contains the submitted value of a previous field | +| Insert tag | Description | Example | +|------------------------------------------------------|--------------------------------------------------|----------| +| `{{mp_forms::::step::current}}` | Contains the current step you are on | 2 | +| `{{mp_forms::::step::total}}` | Contains the total steps of your form | 5 | +| `{{mp_forms::::step::percentage}}` | Contains the percentage of your progress | 20 | +| `{{mp_forms::::step::numbers}}` | Contains a classic `x of y` display | `2 / 5` | +| `{{mp_forms::::step::label}}` | Contains the label of the current step | `Step 1` | +| `{{mp_forms::::field_value::}}` | Contains the submitted value of a previous field | | Note that they can be especially useful together with a `Custom HTML` front end module. Let's assume you want to display a progress bar for form ID `5`: diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..efbb97b --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,24 @@ +# Upgrading from 4.x to 5.x + +## BC Breaks + +### Developers + +The code base received a complete rewrite from scratch. If you worked with the `MPFormsFormManager` before, go for +proper DI and the `FormManagerFactoryInterface` to access the `FormManager` for a given form ID: + +``` +$manager = $this->formManagerFactory->forFormId($formId); +``` + +### Users + +- The `MPForms - Steps` front end module does not provide `even` and `odd` CSS classes anymore. +- The `MPForms - Steps` front end module does not provide the `forbidden` CSS class anymore. Instead, you have more clear + `accessible` and `inaccessible` classes plus the current one gets `current` now. +- The template `form_mp_form_page_switch` has been renamed to `form_mp_form_pageswitch` in order to support custom + template selection in the back end. You may need to adjust your custom templates as well. + +## New + +- The insert tag `{{mp_forms::::step::label}}` is new and outputs the label of the current step \ No newline at end of file diff --git a/composer.json b/composer.json index 1bc59dc..25d4e73 100644 --- a/composer.json +++ b/composer.json @@ -1,38 +1,61 @@ { "name": "terminal42/contao-mp_forms", - "description": "mp_forms extension for Contao Open Source CMS", - "keywords": ["contao", "forms", "multi", "multipage"], - "type": "contao-module", + "description": "An extension for Contao Open Source CMS to create multi steps forms using the form generator", + "keywords": ["contao", "forms", "multi", "multipage", "step"], + "type": "contao-bundle", "license": "LGPL-3.0+", "authors":[ { "name":"terminal42 gmbh", - "homepage":"http://terminal42.ch" + "homepage":"https://www.terminal42.ch" } ], + "funding": [ + { + "type": "github", + "url": "https://github.com/terminal42" + }, + { + "type": "other", + "url": "https://ko-fi.com/terminal42" + } + ], + "support": { + "issues": "https://github.com/terminal42/contao-mp_forms/issues", + "source": "https://github.com/terminal42/contao-mp_forms", + "forum": "https://community.contao.org" + }, "require": { - "php": "^7.1 || ^8.0", - "contao/core-bundle": "^4.9", - "contao-community-alliance/composer-plugin": "^2.4 || ^3.0", - "codefog/contao-haste": "^4.18", - "symfony/var-dumper": "^4.4 || ^5.0", - "symfony/property-access": "^4.4 || ^5.0" + "php": "^8.1", + "contao/core-bundle": "^4.13 || ^5.0.8", + "codefog/contao-haste": "^5.0", + "symfony/filesystem": "^5.4 || ^6.0", + "symfony/var-dumper": "^5.4 || ^6.0" }, "autoload": { - "files": [ - "FormMPFormPageSwitch.php", - "FormMPFormPlaceholder.php", - "MPForms.php", - "MPFormsFormManager.php", - "MPFormsSessionManager.php", - "MPFormsStepsModule.php" - ] + "psr-4": { + "Terminal42\\MultipageFormsBundle\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Terminal42\\MultipageFormsBundle\\Test\\": "tests/" + } }, "extra": { - "contao": { - "sources": { - "": "system/modules/mp_forms" - } + "contao-manager-plugin": "Terminal42\\MultipageFormsBundle\\ContaoManager\\Plugin" + }, + "scripts": { + "cs-fixer": "@php tools/ecs/vendor/bin/ecs check config/ contao/ src/ tests/ --config tools/ecs/config.php --fix --ansi", + "unit-tests": "@php tools/phpunit/vendor/bin/phpunit -c tools/phpunit/phpunit.xml.dist" + }, + "config": { + "allow-plugins": { + "contao-components/installer": false, + "contao/manager-plugin": false } + }, + "require-dev": { + "contao/manager-plugin": "^2.12" } } diff --git a/config/autoload.ini b/config/autoload.ini deleted file mode 100644 index 8fc5d48..0000000 --- a/config/autoload.ini +++ /dev/null @@ -1,13 +0,0 @@ -;; -; List modules which are required to be loaded beforehand -;; -requires[] = "core" -requires[] = "haste" - - -;; -; Configure what you want the autoload creator to register -;; -register_namespaces = false -register_classes = false -register_templates = false \ No newline at end of file diff --git a/config/config.php b/config/config.php deleted file mode 100644 index 2b583db..0000000 --- a/config/config.php +++ /dev/null @@ -1,29 +0,0 @@ - - * @license http://opensource.org/licenses/lgpl-3.0.html LGPL - * @link https://github.com/terminal42/contao-mp_forms - */ - -/** - * Form fields - */ -$GLOBALS['TL_FFL']['mp_form_pageswitch'] = 'FormMPFormPageSwitch'; -$GLOBALS['TL_FFL']['mp_form_placeholder'] = 'FormMPFormPlaceholder'; - -/** - * Front end modules - */ -$GLOBALS['FE_MOD']['application']['mp_form_steps'] = 'MPFormsStepsModule'; - -/** - * Hooks - */ -$GLOBALS['TL_HOOKS']['replaceInsertTags'][] = ['MPForms', 'replaceTags']; -$GLOBALS['TL_HOOKS']['compileFormFields'][] = ['MPForms', 'compileFormFields']; -$GLOBALS['TL_HOOKS']['loadFormField'][] = ['MPForms', 'loadValuesFromSession']; -$GLOBALS['TL_HOOKS']['prepareFormData'][] = ['MPForms', 'prepareFormData']; diff --git a/config/services.php b/config/services.php new file mode 100644 index 0000000..a70886f --- /dev/null +++ b/config/services.php @@ -0,0 +1,70 @@ +services(); + + $services->defaults()->autoconfigure(); + + $services + ->set(FormManagerFactory::class) + ->args([ + service('contao.framework'), + service('request_stack'), + service(UrlParser::class), + ]) + ->public() + ->alias(FormManagerFactoryInterface::class, FormManagerFactory::class) + ->public() + ; + + $services + ->set(InsertTagsListener::class) + ->args([ + service(FormManagerFactoryInterface::class), + ]) + ; + + $services + ->set(LoadFormFieldListener::class) + ->args([ + service(FormManagerFactoryInterface::class), + ]) + ; + + $services + ->set(CompileFormFieldsListener::class) + ->args([ + service(FormManagerFactoryInterface::class), + service('request_stack'), + ]) + ; + + $services + ->set(PrepareFomDataListener::class) + ->args([ + service(FormManagerFactoryInterface::class), + service('request_stack'), + ]) + ; + + $services + ->set(StepsController::class) + ->args([ + service('contao.framework'), + service(FormManagerFactoryInterface::class), + ]) + ; +}; diff --git a/contao/config/config.php b/contao/config/config.php new file mode 100644 index 0000000..999768a --- /dev/null +++ b/contao/config/config.php @@ -0,0 +1,12 @@ +addLegend('mp_forms_legend', null, PaletteManipulator::POSITION_AFTER, true) + ->addField('mp_forms_getParam', 'mp_forms_legend') + ->addField('mp_forms_sessionRefParam', 'mp_forms_legend') + ->addField('mp_forms_backFragment', 'mp_forms_legend') + ->addField('mp_forms_nextFragment', 'mp_forms_legend') + ->applyToPalette('default', 'tl_form') +; + +$GLOBALS['TL_DCA']['tl_form']['fields']['mp_forms_getParam'] = [ + 'exclude' => true, + 'default' => 'step', + 'inputType' => 'text', + 'eval' => ['tl_class' => 'w50', 'maxlength' => 255], + 'sql' => ['type' => 'string', 'length' => 255, 'default' => 'step', 'notnull' => true], +]; + +$GLOBALS['TL_DCA']['tl_form']['fields']['mp_forms_sessionRefParam'] = [ + 'exclude' => true, + 'default' => 'ref', + 'inputType' => 'text', + 'eval' => ['tl_class' => 'w50', 'maxlength' => 255], + 'sql' => ['type' => 'string', 'length' => 255, 'default' => 'ref', 'notnull' => true], +]; + +$GLOBALS['TL_DCA']['tl_form']['fields']['mp_forms_backFragment'] = [ + 'exclude' => true, + 'inputType' => 'text', + 'eval' => ['tl_class' => 'w50', 'maxlength' => 255], + 'sql' => "varchar(255) NOT NULL default ''", +]; + +$GLOBALS['TL_DCA']['tl_form']['fields']['mp_forms_nextFragment'] = [ + 'exclude' => true, + 'inputType' => 'text', + 'eval' => ['tl_class' => 'w50', 'maxlength' => 255], + 'sql' => "varchar(255) NOT NULL default ''", +]; diff --git a/contao/dca/tl_form_field.php b/contao/dca/tl_form_field.php new file mode 100644 index 0000000..7327447 --- /dev/null +++ b/contao/dca/tl_form_field.php @@ -0,0 +1,27 @@ + true, + 'inputType' => 'text', + 'eval' => ['tl_class' => 'w50 clr', 'maxlength' => 255], + 'sql' => ['type' => 'string', 'length' => 255, 'default' => '', 'notnull' => true], +]; + +$GLOBALS['TL_DCA']['tl_form_field']['fields']['mp_forms_backFragment'] = [ + 'exclude' => true, + 'inputType' => 'text', + 'eval' => ['tl_class' => 'w50', 'maxlength' => 255], + 'sql' => ['type' => 'string', 'length' => 255, 'default' => '', 'notnull' => true], +]; + +$GLOBALS['TL_DCA']['tl_form_field']['fields']['mp_forms_nextFragment'] = [ + 'exclude' => true, + 'inputType' => 'text', + 'eval' => ['tl_class' => 'w50', 'maxlength' => 255], + 'sql' => ['type' => 'string', 'length' => 255, 'default' => '', 'notnull' => true], +]; diff --git a/contao/dca/tl_module.php b/contao/dca/tl_module.php new file mode 100644 index 0000000..eea657d --- /dev/null +++ b/contao/dca/tl_module.php @@ -0,0 +1,10 @@ + - * @license http://opensource.org/licenses/lgpl-3.0.html LGPL - * @link https://github.com/terminal42/contao-mp_forms - */ +declare(strict_types=1); -/** +/* * Legends */ $GLOBALS['TL_LANG']['tl_form']['mp_forms_legend'] = 'Mehrseitige Formulare'; -/** +/* * Fields */ $GLOBALS['TL_LANG']['tl_form']['mp_forms_getParam'] = ['Schritt Query-Parameter', 'Sie können hier optional den verwendeten Query-Parameter für die Schritte umstellen.']; $GLOBALS['TL_LANG']['tl_form']['mp_forms_sessionRefParam'] = ['Session-Referenz Query-Parameter', 'Um gleichzeitiges Editieren in mehreren Tabs zu erlauben, muss eine Referenz übergeben werden. Hier steuern Sie den Namen des Query-Parameters.']; $GLOBALS['TL_LANG']['tl_form']['mp_forms_backFragment'] = ['URL Fragment für die Zurück-Schaltfläche', 'Hier können Sie ein optionales URL Fragment eingeben, welches der URL hinzugefügt wird (z.B. für Sprunglinks). Lassen Sie "#" weg.']; -$GLOBALS['TL_LANG']['tl_form']['mp_forms_nextFragment'] = ['URL Fragment für die Weiter-Schaltfläche', 'Hier können Sie ein optionales URL Fragment eingeben, welches der URL hinzugefügt wird (z.B. für Sprunglinks). Lassen Sie "#" weg.']; \ No newline at end of file +$GLOBALS['TL_LANG']['tl_form']['mp_forms_nextFragment'] = ['URL Fragment für die Weiter-Schaltfläche', 'Hier können Sie ein optionales URL Fragment eingeben, welches der URL hinzugefügt wird (z.B. für Sprunglinks). Lassen Sie "#" weg.']; diff --git a/languages/de/tl_form_field.php b/contao/languages/de/tl_form_field.php similarity index 71% rename from languages/de/tl_form_field.php rename to contao/languages/de/tl_form_field.php index 4c71f46..f153f5f 100644 --- a/languages/de/tl_form_field.php +++ b/contao/languages/de/tl_form_field.php @@ -1,23 +1,16 @@ - * @license http://opensource.org/licenses/lgpl-3.0.html LGPL - * @link https://github.com/terminal42/contao-mp_forms - */ +declare(strict_types=1); -/** +/* * Form fields */ $GLOBALS['TL_LANG']['FFL']['mp_form_pageswitch'] = ['Seitenumbruch', 'Trennt die Felder in mehrere Seiten.']; $GLOBALS['TL_LANG']['FFL']['mp_form_placeholder'] = ['Zusammenfassung', 'Erlaubt die Ausgabe von Daten der vorderen Seite(n).']; -/** +/* * Fields */ $GLOBALS['TL_LANG']['tl_form_field']['mp_forms_backButton'] = ['Bezeichnung der Zurück-Schaltfläche', 'Bitte geben Sie die Bezeichnung der Zurück-Schaltfläche ein.']; $GLOBALS['TL_LANG']['tl_form_field']['mp_forms_backFragment'] = ['URL Fragment für die Zurück-Schaltfläche', 'Hier können Sie ein optionales URL Fragment eingeben, welches der URL hinzugefügt wird (z.B. für Sprunglinks). Lassen Sie "#" weg.']; -$GLOBALS['TL_LANG']['tl_form_field']['mp_forms_nextFragment'] = ['URL Fragment für die Weiter-Schaltfläche', 'Hier können Sie ein optionales URL Fragment eingeben, welches der URL hinzugefügt wird (z.B. für Sprunglinks). Lassen Sie "#" weg.']; \ No newline at end of file +$GLOBALS['TL_LANG']['tl_form_field']['mp_forms_nextFragment'] = ['URL Fragment für die Weiter-Schaltfläche', 'Hier können Sie ein optionales URL Fragment eingeben, welches der URL hinzugefügt wird (z.B. für Sprunglinks). Lassen Sie "#" weg.']; diff --git a/contao/languages/en/modules.php b/contao/languages/en/modules.php new file mode 100644 index 0000000..c31bec8 --- /dev/null +++ b/contao/languages/en/modules.php @@ -0,0 +1,8 @@ + - * @license http://opensource.org/licenses/lgpl-3.0.html LGPL - * @link https://github.com/terminal42/contao-mp_forms - */ +declare(strict_types=1); -/** +/* * Legends */ $GLOBALS['TL_LANG']['tl_form']['mp_forms_legend'] = 'Multiple page forms'; -/** +/* * Fields */ $GLOBALS['TL_LANG']['tl_form']['mp_forms_getParam'] = ['Step query parameter', 'You can optionally modify the used query parameter for the steps here.']; diff --git a/languages/en/tl_form_field.php b/contao/languages/en/tl_form_field.php similarity index 70% rename from languages/en/tl_form_field.php rename to contao/languages/en/tl_form_field.php index e3ce534..ffde612 100644 --- a/languages/en/tl_form_field.php +++ b/contao/languages/en/tl_form_field.php @@ -1,23 +1,16 @@ - * @license http://opensource.org/licenses/lgpl-3.0.html LGPL - * @link https://github.com/terminal42/contao-mp_forms - */ +declare(strict_types=1); -/** +/* * Form fields */ $GLOBALS['TL_LANG']['FFL']['mp_form_pageswitch'] = ['Page break', 'Separates the form fields into different pages/steps.']; $GLOBALS['TL_LANG']['FFL']['mp_form_placeholder'] = ['Summary', 'Allows the output of the form data of the previous page(s).']; -/** +/* * Fields */ $GLOBALS['TL_LANG']['tl_form_field']['mp_forms_backButton'] = ['Back button label', 'Please enter the label of the back button.']; $GLOBALS['TL_LANG']['tl_form_field']['mp_forms_backFragment'] = ['Back button URL fragment', 'You may enter an optional URL fragment here which will be added to the URL when hitting the back button (e.g. for anchor links). Omit the "#" here.']; -$GLOBALS['TL_LANG']['tl_form_field']['mp_forms_nextFragment'] = ['Continue button URL fragment', 'You may enter an optional URL fragment here which will be added to the URL when hitting the continue button (e.g. for anchor links). Omit the "#" here.']; \ No newline at end of file +$GLOBALS['TL_LANG']['tl_form_field']['mp_forms_nextFragment'] = ['Continue button URL fragment', 'You may enter an optional URL fragment here which will be added to the URL when hitting the continue button (e.g. for anchor links). Omit the "#" here.']; diff --git a/contao/languages/ja/modules.php b/contao/languages/ja/modules.php new file mode 100644 index 0000000..228d05b --- /dev/null +++ b/contao/languages/ja/modules.php @@ -0,0 +1,8 @@ + - * @license http://opensource.org/licenses/lgpl-3.0.html LGPL - * @link https://github.com/terminal42/contao-mp_forms - */ +declare(strict_types=1); -/** +/* * Legends */ $GLOBALS['TL_LANG']['tl_form']['mp_forms_legend'] = '複数ページのフォーム'; -/** +/* * Fields */ $GLOBALS['TL_LANG']['tl_form']['mp_forms_getParam'] = ['段階の問い合わせのパラメーター', -'ここで段階に使用する問い合わせパラメーターを変更できます。']; + 'ここで段階に使用する問い合わせパラメーターを変更できます。', ]; $GLOBALS['TL_LANG']['tl_form']['mp_forms_sessionRefParam'] = ['セッション参照の問い合わせのパラメータ', '複数のタブで同時の編集を有効にするには参照が必要です。この設定で問い合わせのパラメーターの名前を調整してください。']; $GLOBALS['TL_LANG']['tl_form']['mp_forms_backFragment'] = ['戻るボタンのURLの断片', '戻るボタンをクリックしたときのURLに付加するURLの断片(例えば、アンカーのリンク)を入力できます。"#"とすると省略できます。']; $GLOBALS['TL_LANG']['tl_form']['mp_forms_nextFragment'] = ['続けるボタンのURLの断片', '続けるボタンをクリックしたときのURLに付加するURLの断片(例えば、アンカーのリンク)を入力できます。"#"とすると省略できます。']; diff --git a/languages/ja/tl_form_field.php b/contao/languages/ja/tl_form_field.php similarity index 75% rename from languages/ja/tl_form_field.php rename to contao/languages/ja/tl_form_field.php index f27373b..fe89902 100644 --- a/languages/ja/tl_form_field.php +++ b/contao/languages/ja/tl_form_field.php @@ -1,23 +1,16 @@ - * @license http://opensource.org/licenses/lgpl-3.0.html LGPL - * @link https://github.com/terminal42/contao-mp_forms - */ +declare(strict_types=1); -/** +/* * Form fields */ $GLOBALS['TL_LANG']['FFL']['mp_form_pageswitch'] = ['改ページ', 'フォームの項目を異るページ/段階に分割します。']; $GLOBALS['TL_LANG']['FFL']['mp_form_placeholder'] = ['要約', '以前のページのフォームのデータを表示します。']; -/** +/* * Fields */ $GLOBALS['TL_LANG']['tl_form_field']['mp_forms_backButton'] = ['戻るボタンのラベル', '戻るボタンのラベルを入力してください。']; $GLOBALS['TL_LANG']['tl_form_field']['mp_forms_backFragment'] = ['戻るボタンのURLの断片', '戻るボタンをクリックしたときのURLに付加するURLの断片(例えば、アンカーのリンク)を入力できます。"#"とすると省略できます。']; -$GLOBALS['TL_LANG']['tl_form_field']['mp_forms_nextFragment'] = ['続けるボタンのURLの断片', '続けるボタンをクリックしたときのURLに付加するURLの断片(例えば、アンカーのリンク)を入力できます。"#"とすると省略できます。']; \ No newline at end of file +$GLOBALS['TL_LANG']['tl_form_field']['mp_forms_nextFragment'] = ['続けるボタンのURLの断片', '続けるボタンをクリックしたときのURLに付加するURLの断片(例えば、アンカーのリンク)を入力できます。"#"とすると省略できます。']; diff --git a/templates/form_mp_form_page_switch.html5 b/contao/templates/form_mp_form_pageswitch.html5 similarity index 100% rename from templates/form_mp_form_page_switch.html5 rename to contao/templates/form_mp_form_pageswitch.html5 diff --git a/templates/form_mp_form_placeholder.html5 b/contao/templates/form_mp_form_placeholder.html5 similarity index 100% rename from templates/form_mp_form_placeholder.html5 rename to contao/templates/form_mp_form_placeholder.html5 diff --git a/templates/mod_mp_form_steps.html5 b/contao/templates/mod_mp_form_steps.html5 similarity index 79% rename from templates/mod_mp_form_steps.html5 rename to contao/templates/mod_mp_form_steps.html5 index a397eb0..9238a1b 100644 --- a/templates/mod_mp_form_steps.html5 +++ b/contao/templates/mod_mp_form_steps.html5 @@ -4,4 +4,4 @@ navigation ?> -endblock(); ?> +endblock(); ?> \ No newline at end of file diff --git a/dca/tl_form.php b/dca/tl_form.php deleted file mode 100644 index 1d972db..0000000 --- a/dca/tl_form.php +++ /dev/null @@ -1,50 +0,0 @@ - - * @license http://opensource.org/licenses/lgpl-3.0.html LGPL - * @link https://github.com/terminal42/contao-mp_forms - */ - -/** - * Table tl_form - */ -$GLOBALS['TL_DCA']['tl_form']['config']['onsubmit_callback'][] = function($dc) { - $manager = new \MPFormsFormManager((int) $dc->id); - $manager->resetData(); -}; - -$GLOBALS['TL_DCA']['tl_form']['palettes']['default'] .= ';{mp_forms_legend},mp_forms_getParam,mp_forms_sessionRefParam,mp_forms_backFragment,mp_forms_nextFragment'; - -$GLOBALS['TL_DCA']['tl_form']['fields']['mp_forms_getParam'] = [ - 'exclude' => true, - 'default' => 'step', - 'inputType' => 'text', - 'eval' => ['tl_class' => 'w50', 'maxlength' => 255], - 'sql' => "varchar(255) NOT NULL default 'step'" -]; - -$GLOBALS['TL_DCA']['tl_form']['fields']['mp_forms_sessionRefParam'] = [ - 'exclude' => true, - 'default' => 'ref', - 'inputType' => 'text', - 'eval' => ['tl_class' => 'w50', 'maxlength' => 255], - 'sql' => "varchar(255) NOT NULL default 'ref'" -]; - -$GLOBALS['TL_DCA']['tl_form']['fields']['mp_forms_backFragment'] = [ - 'exclude' => true, - 'inputType' => 'text', - 'eval' => ['tl_class' => 'w50', 'maxlength' => 255], - 'sql' => "varchar(255) NOT NULL default ''" -]; - -$GLOBALS['TL_DCA']['tl_form']['fields']['mp_forms_nextFragment'] = [ - 'exclude' => true, - 'inputType' => 'text', - 'eval' => ['tl_class' => 'w50', 'maxlength' => 255], - 'sql' => "varchar(255) NOT NULL default ''" -]; \ No newline at end of file diff --git a/dca/tl_form_field.php b/dca/tl_form_field.php deleted file mode 100644 index b808f8f..0000000 --- a/dca/tl_form_field.php +++ /dev/null @@ -1,41 +0,0 @@ - - * @license http://opensource.org/licenses/lgpl-3.0.html LGPL - * @link https://github.com/terminal42/contao-mp_forms - */ - -/** - * Table tl_form_field - */ -$GLOBALS['TL_DCA']['tl_form_field']['config']['onsubmit_callback'][] = function($dc) { - $manager = new \MPFormsFormManager((int) $dc->activeRecord->pid); - $manager->resetData(); -}; -$GLOBALS['TL_DCA']['tl_form_field']['palettes']['mp_form_pageswitch'] = '{type_legend},type,label,mp_forms_backButton,slabel;{image_legend:hide},imageSubmit;{expert_legend:hide},mp_forms_backFragment,mp_forms_nextFragment,class,accesskey,tabindex;{template_legend:hide},customTpl;{invisible_legend:hide},invisible'; -$GLOBALS['TL_DCA']['tl_form_field']['palettes']['mp_form_placeholder'] = '{type_legend},type;{text_legend},html;{template_legend:hide},customTpl;{invisible_legend:hide},invisible'; - -$GLOBALS['TL_DCA']['tl_form_field']['fields']['mp_forms_backButton'] = [ - 'exclude' => true, - 'inputType' => 'text', - 'eval' => ['tl_class' => 'w50 clr', 'maxlength' => 255], - 'sql' => "varchar(255) NOT NULL default ''" -]; - -$GLOBALS['TL_DCA']['tl_form_field']['fields']['mp_forms_backFragment'] = [ - 'exclude' => true, - 'inputType' => 'text', - 'eval' => ['tl_class' => 'w50', 'maxlength' => 255], - 'sql' => "varchar(255) NOT NULL default ''" -]; - -$GLOBALS['TL_DCA']['tl_form_field']['fields']['mp_forms_nextFragment'] = [ - 'exclude' => true, - 'inputType' => 'text', - 'eval' => ['tl_class' => 'w50', 'maxlength' => 255], - 'sql' => "varchar(255) NOT NULL default ''" -]; diff --git a/dca/tl_module.php b/dca/tl_module.php deleted file mode 100644 index 13021fa..0000000 --- a/dca/tl_module.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @license http://opensource.org/licenses/lgpl-3.0.html LGPL - * @link https://github.com/terminal42/contao-mp_forms - */ - -/** - * Table tl_module - */ -$GLOBALS['TL_DCA']['tl_module']['palettes']['mp_form_steps'] = ' -{title_legend},name,headline,type; -{config_legend},form,navigationTpl; -{template_legend:hide},customTpl; -{protected_legend:hide},protected; -{expert_legend:hide},guests,cssID,space'; diff --git a/languages/de/modules.php b/languages/de/modules.php deleted file mode 100644 index 5b3dae4..0000000 --- a/languages/de/modules.php +++ /dev/null @@ -1,15 +0,0 @@ - - * @license http://opensource.org/licenses/lgpl-3.0.html LGPL - * @link https://github.com/terminal42/contao-mp_forms - */ - -/** - * Front end modules - */ -$GLOBALS['TL_LANG']['FMD']['mp_form_steps'] = ['MPForms - Schritte', 'Zeigt eine Navigation für die Schritte eines mehrseitigen Formulars an.']; diff --git a/languages/en/modules.php b/languages/en/modules.php deleted file mode 100644 index 6885d69..0000000 --- a/languages/en/modules.php +++ /dev/null @@ -1,15 +0,0 @@ - - * @license http://opensource.org/licenses/lgpl-3.0.html LGPL - * @link https://github.com/terminal42/contao-mp_forms - */ - -/** - * Front end modules - */ -$GLOBALS['TL_LANG']['FMD']['mp_form_steps'] = ['MPForms - Steps', 'Displays a step navigation for a given multipage form.']; diff --git a/languages/ja/modules.php b/languages/ja/modules.php deleted file mode 100644 index dc75d84..0000000 --- a/languages/ja/modules.php +++ /dev/null @@ -1,15 +0,0 @@ - - * @license http://opensource.org/licenses/lgpl-3.0.html LGPL - * @link https://github.com/terminal42/contao-mp_forms - */ - -/** - * Front end modules - */ -$GLOBALS['TL_LANG']['FMD']['mp_form_steps'] = ['MPFormsの段階', '指定した複数ページにフォームの段階を示すナビゲーションを表示']; diff --git a/src/ContaoManager/Plugin.php b/src/ContaoManager/Plugin.php new file mode 100644 index 0000000..25d83dc --- /dev/null +++ b/src/ContaoManager/Plugin.php @@ -0,0 +1,24 @@ +setLoadAfter([ + ContaoCoreBundle::class, + ]), + ]; + } +} diff --git a/src/Controller/FrontendModule/StepsController.php b/src/Controller/FrontendModule/StepsController.php new file mode 100644 index 0000000..5b3fd4b --- /dev/null +++ b/src/Controller/FrontendModule/StepsController.php @@ -0,0 +1,81 @@ +formManagerFactory->forFormId($model->form); + + if (!$manager->isValidFormFieldCombination()) { + return new Response('', Response::HTTP_NO_CONTENT); + } + + $navTpl = $this->contaoFramework->createInstance(FrontendTemplate::class, [$model->navigationTpl ?: 'nav_default']); + $navTpl->level = 0; + $navTpl->items = $this->buildNavigationItems($manager); + $template->navigation = $navTpl->parse(); + + return $template->getResponse(); + } + + private function buildNavigationItems(FormManager $manager) + { + $steps = range(0, $manager->getNumberOfSteps() - 1); + $firstFailingStep = $manager->getFirstInvalidStep(); + + $items = []; + + foreach ($steps as $step) { + $cssClasses = []; + $cssClasses[] = 'step_'.$step; + + // Check if step can be accessed + $canBeAccessed = $step <= $firstFailingStep; + + $isCurrent = $step === $manager->getCurrentStep(); + + if ($isCurrent) { + $cssClasses[] = 'current'; + } else { + $cssClasses[] = $canBeAccessed ? 'accessible' : 'inaccessible'; + } + + // Link only if it's not the current step and it can be accessed + $shouldDisplayLink = !$isCurrent && $canBeAccessed; + + $items[] = [ + 'isActive' => !$shouldDisplayLink, // isActive causes a instead of , so we negate + 'class' => implode(' ', $cssClasses), + 'href' => $manager->getUrlForStep($step), + 'pageTitle' => $manager->getLabelForStep($step), + 'title' => $manager->getLabelForStep($step), + 'link' => $manager->getLabelForStep($step), + 'nofollow' => true, + 'accesskey' => '', + 'target' => '', + ]; + } + + return $items; + } +} diff --git a/src/DependencyInjection/Terminal42MultipageFormsExtension.php b/src/DependencyInjection/Terminal42MultipageFormsExtension.php new file mode 100644 index 0000000..5dd76d5 --- /dev/null +++ b/src/DependencyInjection/Terminal42MultipageFormsExtension.php @@ -0,0 +1,22 @@ + $configs + */ + public function load(array $configs, ContainerBuilder $container): void + { + $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../../config')); + $loader->load('services.php'); + } +} diff --git a/src/EventListener/CompileFormFieldsListener.php b/src/EventListener/CompileFormFieldsListener.php new file mode 100644 index 0000000..9f06c96 --- /dev/null +++ b/src/EventListener/CompileFormFieldsListener.php @@ -0,0 +1,82 @@ + $formFields + * + * @return array + */ + public function __invoke(array $formFields, string $formId, Form $form): array + { + if (0 === \count($formFields)) { + return $formFields; + } + + $request = $this->requestStack->getCurrentRequest(); + + if (!$request instanceof Request) { + return $formFields; + } + + $manager = $this->formManagerFactory->forFormId((int) $form->id); + + // If the manager is currently being prepared (recursive compileFormFields hook call), we abort + if ($manager->isPreparing()) { + return $formFields; + } + + // Don't try to render multi page form if no valid combination + if (!$manager->isValidFormFieldCombination()) { + return $manager->getFieldsWithoutPageBreaks(); + } + + // Validate whether previous form data was submitted if we're not on the first step. + // This has to be done no matter if we're in a POST request right now or not as otherwise + // you can submit a POST request without any previous step data (e.g. by deleting the session cookie + // manually) + if (!$manager->isFirstStep()) { + $firstInvalidStep = $manager->getFirstInvalidStep(); + + if ($firstInvalidStep < $manager->getCurrentStep()) { + $manager->redirectToStep($firstInvalidStep); + } + } + + $stepData = $manager->getDataOfCurrentStep(); + + // If there is form data submitted in this step, store the original values here no matter if we're going back or if we continue. + // Important: We do not store $_FILES here! The problem with storing $_FILES across requests is that we would need + // to move it from its tmp_name as PHP deletes files automatically after the request has finished. We could indeed + // move them here but if we did at this stage the form fields themselves would later not be able to move them + // to their own desired place. So we cannot store any file information at this stage. + if ($_POST) { + $stepData = $stepData->withOriginalPostData(new ParameterBag($_POST)); + $manager->storeStepData($stepData); + } + + // Redirect back if asked for it + if ('back' === $request->request->get('mp_form_pageswitch')) { + $manager->redirectToStep($manager->getPreviousStep()); + } + + return $manager->getFieldsForStep($manager->getCurrentStep()); + } +} diff --git a/src/EventListener/InsertTagsListener.php b/src/EventListener/InsertTagsListener.php new file mode 100644 index 0000000..7e76f15 --- /dev/null +++ b/src/EventListener/InsertTagsListener.php @@ -0,0 +1,52 @@ +formManagerFactory->forFormId((int) $formId); + + switch ($type) { + case 'step': + return $this->getStepValue($manager, $value); + case 'field_value': + return $manager->getDataOfAllSteps()->getAllSubmitted()[$value] ?? ''; + } + + return ''; + } + + private function getStepValue(FormManager $manager, string $value): string + { + return (string) match ($value) { + 'current' => $manager->getCurrentStep() + 1, + 'total' => $manager->getNumberOfSteps(), + 'percentage' => ($manager->getCurrentStep() + 1) / $manager->getNumberOfSteps() * 100, + 'numbers' => ($manager->getCurrentStep() + 1).' / '.$manager->getNumberOfSteps(), + 'label' => $manager->getLabelForStep($manager->getCurrentStep()), + default => '', + }; + } +} diff --git a/src/EventListener/LoadFormFieldListener.php b/src/EventListener/LoadFormFieldListener.php new file mode 100644 index 0000000..24db813 --- /dev/null +++ b/src/EventListener/LoadFormFieldListener.php @@ -0,0 +1,43 @@ +name)) { + return $widget; + } + + $postData = new ParameterBag($_POST); + + $manager = $this->formManagerFactory->forFormId((int) $form->id); + $stepData = $manager->getDataOfCurrentStep(); + + // We only prefill the value if it was not submitted in this step. + // If you submit a value in step 1, go to step 2, then go back to step 1 and submit a wrong value there, Contao + // would display an error, but we'd prefill it again with the previous value which would make no sense. + // Moreover, we prefill with submitted data as priority (= validated submitted widget data) and otherwise fall + // back to potential previous post data which has not been validated yet (e.g. you filled in the values on step 2 + // but then navigated back) + if (!$postData->has($widget->name)) { + $widget->value = $stepData->getSubmitted()->get($widget->name, $stepData->getOriginalPostData()->get($widget->name)); + } + + return $widget; + } +} diff --git a/src/EventListener/PrepareFomDataListener.php b/src/EventListener/PrepareFomDataListener.php new file mode 100644 index 0000000..5120763 --- /dev/null +++ b/src/EventListener/PrepareFomDataListener.php @@ -0,0 +1,97 @@ + $fields + */ + public function __invoke(array &$submitted, array &$labels, array $fields, Form $form, array &$files = []): void + { + $manager = $this->formManagerFactory->forFormId((int) $form->id); + + // Don't do anything if not valid + if (!$manager->isValidFormFieldCombination()) { + return; + } + + $submittedBag = new ParameterBag($submitted); + $labelsBag = new ParameterBag($labels); + + $pageSwitchValue = $submittedBag->get('mp_form_pageswitch', ''); + $submittedBag->remove('mp_form_pageswitch'); + + // Store data in session + $stepData = $manager->getDataOfCurrentStep(); + $stepData = $stepData->withSubmitted($submittedBag); + $stepData = $stepData->withLabels($labelsBag); + + $stepData = $stepData->withFiles($this->getUploadedFiles($files)); + + $manager->storeStepData($stepData); + + // Submit form + if ($manager->isLastStep() && 'continue' === $pageSwitchValue) { + $allData = $manager->getDataOfAllSteps(); + + // Replace data by reference and then return so the default Contao + // routine kicks in + $submitted = $allData->getAllSubmitted(); + $labels = $allData->getAllLabels(); + $files = $allData->getAllFiles(); + + // Add session data for Contao 4.13 + if (version_compare(ContaoCoreBundle::getVersion(), '5.0', '<')) { + // Override $_SESSION['FORM_DATA'] so it contains the data of + // previous steps as well + $_SESSION['FORM_DATA'] = $submitted; + $_SESSION['FILES'] = $allData->getAllFiles(); + } + + // End session + $manager->endSession(); + + return; + } + + $manager->redirectToStep($manager->getNextStep()); + } + + private function getUploadedFiles($hook = []): FileParameterBag + { + // Contao 5 + if (0 !== \count($hook)) { + return new FileParameterBag($hook); + } + + // Contao 4.13 + $request = $this->reqestStack->getCurrentRequest(); + + if (null === $request) { + return new FileParameterBag(); + } + + if (!$request->getSession()->isStarted()) { + return new FileParameterBag(); + } + + return new FileParameterBag($_SESSION['FILES'] ?? []); + } +} diff --git a/src/FormManager.php b/src/FormManager.php new file mode 100644 index 0000000..456061d --- /dev/null +++ b/src/FormManager.php @@ -0,0 +1,459 @@ + + */ + private array $formFieldModels; + + /** + * @var array> + */ + private array $formFieldsPerStep = []; + + private bool $isValidFormFieldCombination = true; + + public function __construct( + private int $formId, + private Request $request, + private ContaoFramework $contaoFramework, + private StorageInterface $storage, + private StorageIdentifierGeneratorInterface $storageIdentifierGenerator, + private SessionReferenceGeneratorInterface $sessionReferenceGenerator, + private UrlParser $urlParser, + ) { + } + + public function isFirstStep(): bool + { + $this->prepare(); + + return 0 === $this->getCurrentStep(); + } + + public function getDataOfAllSteps(): StepDataCollection + { + $this->prepare(); + + return $this->storage->getData($this->storageIdentifier); + } + + public function getCurrentStep(): int + { + $this->prepare(); + + return $this->request->query->getInt($this->getGetParam()); + } + + public function getNumberOfSteps(): int + { + $this->prepare(); + + return \count(array_keys($this->formFieldsPerStep)); + } + + public function isValidFormFieldCombination(): bool + { + $this->prepare(); + + return $this->isValidFormFieldCombination + && $this->getNumberOfSteps() > 1; + } + + /** + * @return array + */ + public function getFieldsWithoutPageBreaks(): array + { + $this->prepare(); + + $formFields = $this->formFieldModels; + + foreach ($formFields as $k => $formField) { + if ('mp_form_pageswitch' === $formField->type) { + unset($formFields[$k]); + } + } + + return $formFields; + } + + public function getDataOfCurrentStep(): StepData + { + return $this->getDataOfStep($this->getCurrentStep()); + } + + public function getDataOfStep(int $step): StepData + { + $this->validateStep($step); + + $stepCollection = $this->storage->getData($this->storageIdentifier); + + return $stepCollection->get($step); + } + + public function storeStepData(StepData $stepData): self + { + $this->prepare(); + + $stepCollection = $this->storage->getData($this->storageIdentifier); + $stepCollection = $stepCollection->set($stepData); + $this->storage->storeData($this->storageIdentifier, $stepCollection); + + return $this; + } + + public function getPreviousStep(): int + { + $this->prepare(); + + $previous = $this->getCurrentStep() - 1; + + if ($previous < 0) { + $previous = 0; + } + + return $previous; + } + + public function getFirstInvalidStep(): int + { + $this->prepare(); + + $steps = range(0, $this->getNumberOfSteps() - 1); + + foreach ($steps as $step) { + $data = $this->getDataOfStep($step); + + if ($data->isEmpty()) { + return $step; + } + } + + return $this->getNumberOfSteps(); + } + + public function hasStep(int $step): bool + { + $this->prepare(); + + return isset($this->formFieldsPerStep[$step]); + } + + /** + * @return array + */ + public function getFieldsForStep(int $step = 0): array + { + $this->validateStep($step); + + return $this->formFieldsPerStep[$step]; + } + + public function isLastStep(): bool + { + $this->prepare(); + + return $this->getCurrentStep() >= $this->getNumberOfSteps() - 1; + } + + public function getNextStep(): int + { + $this->prepare(); + + $next = $this->getCurrentStep() + 1; + + if ($next > $this->getNumberOfSteps()) { + $next = $this->getNumberOfSteps(); + } + + return $next; + } + + public function getLabelForStep(int $step): string + { + $this->validateStep($step); + + foreach ($this->getFieldsForStep($step) as $formField) { + if ($this->isPageBreak($formField) && '' !== $formField->label) { + return $formField->label; + } + } + + return 'Step '.($step + 1); + } + + public function isPreparing(): bool + { + return $this->preparing; + } + + public function getGetParamForSessionReference() + { + return $this->formModel->mp_forms_sessionRefParam ?: 'ref'; + } + + /** + * @throws RedirectResponseException + */ + public function redirectToStep(int $step): void + { + $this->validateStep($step); + + throw new RedirectResponseException($this->getUrlForStep($step)); + } + + public function endSession(): self + { + // Empty storage + $this->storage->storeData($this->storageIdentifier, new StepDataCollection()); + + // Force a new session reference + $this->initSessionReference(true); + + return $this; + } + + public function getUrlForStep(int $step): string + { + $requestUri = urldecode($this->request->getUri()); + + if (0 === $step) { + $url = $this->urlParser->removeQueryString([$this->getGetParam()], $requestUri); + } else { + $url = $this->urlParser->addQueryString($this->getGetParam().'='.$step, $requestUri); + } + + $url = $this->urlParser->addQueryString($this->getGetParamForSessionReference().'='.$this->sessionRef, $url); + + if ($step > $this->getCurrentStep()) { + $fragment = $this->getFragmentForStep($step, 'next'); + } else { + $fragment = $this->getFragmentForStep($this->getCurrentStep(), 'back'); + } + + if ($fragment) { + $url .= '#'.$fragment; + } + + return $url; + } + + /** + * Creates a dummy form instance that is needed for the hooks. + */ + public static function createDummyForm(int $formId): Form + { + return new class($formId) extends Form { + public function __construct(int $formId) + { + // Do not call parent in order to not boot the whole system. Just mock some of it. + $this->id = $formId; + $this->headline = null; + $this->typePrefix = null; + $this->cssID = null; + $this->strColumn = 'main'; + } + }; + } + + /** + * @return array + */ + public function getFormFieldModels(): array + { + $this->prepare(); + + return $this->formFieldModels; + } + + public function getFormId(): string + { + return '' !== $this->formModel->formID ? + 'auto_'.$this->formModel->formID : + 'auto_form_'.$this->formModel->id; + } + + public function getSessionReference(): string + { + $this->prepare(); + + return $this->sessionRef; + } + + /** + * @throws \OutOfBoundsException if the step does not exist + */ + private function validateStep(int $step): void + { + $this->prepare(); + + if (!$this->hasStep($step)) { + throw new \OutOfBoundsException(sprintf('Step %d does not exist.', $step)); + } + } + + private function getGetParam(): string + { + return $this->formModel->mp_forms_getParam ?: 'step'; + } + + private function isPageBreak(FormFieldModel $formField): bool + { + return 'mp_form_pageswitch' === $formField->type; + } + + private function loadFormFieldModels(): void + { + $collection = $this->contaoFramework->getAdapter(FormFieldModel::class)->findPublishedByPid($this->formModel->id); + $formFieldModels = []; + + if (null !== $collection) { + foreach ($collection as $formFieldModel) { + // Ignore the name of form fields which do not use a name (see contao/core-bundle #1268) + if ( + $formFieldModel->name && isset($GLOBALS['TL_DCA']['tl_form_field']['palettes'][$formFieldModel->type]) + && preg_match('/[,;]name[,;]/', $GLOBALS['TL_DCA']['tl_form_field']['palettes'][$formFieldModel->type]) + ) { + $formFieldModels[$formFieldModel->name] = $formFieldModel; + } else { + $formFieldModels[] = $formFieldModel; + } + } + } + + // Needed for the hook + $form = self::createDummyForm($this->formId); + + $systemAdapter = $this->contaoFramework->getAdapter(System::class); + + if (isset($GLOBALS['TL_HOOKS']['compileFormFields']) && \is_array($GLOBALS['TL_HOOKS']['compileFormFields'])) { + foreach ($GLOBALS['TL_HOOKS']['compileFormFields'] as $callback) { + $objCallback = $systemAdapter->importStatic($callback[0]); + $formFieldModels = $objCallback->{$callback[1]}($formFieldModels, $this->getFormId(), $form); + } + } + + $this->formFieldModels = $formFieldModels; + } + + private function prepare(): void + { + if ($this->preparing || $this->prepared) { + return; + } + + $this->preparing = true; + + $formModel = $this->contaoFramework->getAdapter(FormModel::class)->findByPk($this->formId); + + if (null === $formModel) { + throw new \InvalidArgumentException(sprintf('Could not load form ID "%d".', $this->formId)); + } + + $this->formModel = $formModel; + + $this->loadFormFieldModels(); + + if (0 === \count($this->formFieldModels)) { + $this->isValidFormFieldCombination = false; + $this->prepared = true; + $this->preparing = false; + + return; + } + + $i = 0; + + foreach ($this->formFieldModels as $k => $formField) { + $this->formFieldsPerStep[$i][$k] = $formField; + + if ($this->isPageBreak($formField)) { + // Set the name on the model, otherwise one has to enter it + // in the back end every time + $formField->name = $formField->type; + + // Increase counter + ++$i; + } + + // If we have a regular submit form field, that's a misconfiguration + if ('submit' === $formField->type) { + $this->isValidFormFieldCombination = false; + } + } + + // Set current session reference from request or generate a new one + $this->initSessionReference(); + + // Set storage identifier for storage implementations to work with + $this->storageIdentifier = $this->storageIdentifierGenerator->generate($this); + + $this->prepared = true; + $this->preparing = false; + } + + /** + * @param string $mode ("next" or "back") + */ + private function getFragmentForStep(int $step, string $mode): string + { + if (!\in_array($mode, ['back', 'next'], true)) { + throw new \InvalidArgumentException('Mode must be either "back" or "next".'); + } + + $key = sprintf('mp_forms_%sFragment', $mode); + + foreach ($this->getFieldsForStep($step) as $formField) { + if ($this->isPageBreak($formField) && isset($formField->{$key}) && '' !== $formField->{$key}) { + return $formField->{$key}; + } + } + + if (isset($this->formModel->{$key}) && '' !== $this->formModel->{$key}) { + return $this->formModel->{$key}; + } + + return ''; + } + + private function initSessionReference(bool $forceNew = false): void + { + if ($forceNew) { + $this->sessionRef = $this->sessionReferenceGenerator->generate($this); + + return; + } + + $this->sessionRef = $this->request->query->get( + $this->getGetParamForSessionReference(), + $this->sessionReferenceGenerator->generate($this) + ); + } +} diff --git a/src/FormManagerFactory.php b/src/FormManagerFactory.php new file mode 100644 index 0000000..e20704a --- /dev/null +++ b/src/FormManagerFactory.php @@ -0,0 +1,90 @@ + + */ + private array $managers = []; + + public function __construct( + private ContaoFramework $contaoFramework, + private RequestStack $requestStack, + private UrlParser $urlParser, + ) { + } + + public function setStorage(StorageInterface|null $storage): self + { + $this->storage = $storage; + + return $this; + } + + public function setStorageIdentifierGenerator(StorageIdentifierGeneratorInterface|null $storageIdentifierGenerator): self + { + $this->storageIdentifierGenerator = $storageIdentifierGenerator; + + return $this; + } + + public function setSessionReferenceGenerator(SessionReferenceGeneratorInterface|null $sessionReferenceGenerator): self + { + $this->sessionReferenceGenerator = $sessionReferenceGenerator; + + return $this; + } + + public function forFormId(int $id): FormManager + { + $request = $this->requestStack->getCurrentRequest(); + + if (!$request instanceof Request) { + throw new \LogicException('Cannot instantiate a FormManager without a request.'); + } + + if (isset($this->managers[$id])) { + return $this->managers[$id]; + } + + $storage = $this->storage ?? new SessionStorage($request); + $storageIdentifierGenerator = $this->storageIdentifierGenerator ?? new StorageIdentifierGenerator(); + $sessionReferenceGenerator = $this->sessionReferenceGenerator ?? new SessionReferenceGenerator(); + + $manager = $this->managers[$id] = new FormManager( + $id, + $request, + $this->contaoFramework, + $storage, + $storageIdentifierGenerator, + $sessionReferenceGenerator, + $this->urlParser + ); + + if ($storage instanceof FormManagerAwareInterface) { + $storage->setFormManager($manager); + } + + return $manager; + } +} diff --git a/src/FormManagerFactoryInterface.php b/src/FormManagerFactoryInterface.php new file mode 100644 index 0000000..52df503 --- /dev/null +++ b/src/FormManagerFactoryInterface.php @@ -0,0 +1,10 @@ +tempnam(sys_get_temp_dir(), 'nc'); + move_uploaded_file($value['tmp_name'], $target); + $value['tmp_name'] = $target; + } + + return parent::set($name, $value); + } +} diff --git a/src/Step/ParameterBag.php b/src/Step/ParameterBag.php new file mode 100644 index 0000000..e259a2a --- /dev/null +++ b/src/Step/ParameterBag.php @@ -0,0 +1,70 @@ + $v) { + $this->set($k, $v); + } + } + + public function empty(): bool + { + return [] === $this->parameters; + } + + public function clear(): void + { + $this->parameters = []; + } + + public function all(): array + { + return $this->parameters; + } + + public function get(string $name, mixed $default = null): mixed + { + return $this->parameters[$name] ?? $default; + } + + public function set(string $name, mixed $value): self + { + $this->parameters[$name] = $value; + + return $this; + } + + public function has(string $name): bool + { + return \array_key_exists($name, $this->parameters); + } + + public function remove(string $name): self + { + unset($this->parameters[$name]); + + return $this; + } + + public function mergeWith(self $other): self + { + $clone = clone $this; + + $clone->parameters = array_replace_recursive($this->parameters, $other->parameters); + + return $clone; + } + + public function equals(self $other): bool + { + return $this->all() === $other->all(); + } +} diff --git a/src/Step/StepData.php b/src/Step/StepData.php new file mode 100644 index 0000000..42f0491 --- /dev/null +++ b/src/Step/StepData.php @@ -0,0 +1,89 @@ +submitted->empty() && $this->files->empty(); + } + + public function getStep(): int + { + return $this->step; + } + + public function getSubmitted(): ParameterBag + { + return $this->submitted; + } + + public function getLabels(): ParameterBag + { + return $this->labels; + } + + public function getFiles(): ParameterBag + { + return $this->files; + } + + public function getOriginalPostData(): ParameterBag + { + return $this->originalPostData; + } + + public function withOriginalPostData(ParameterBag $originalPostData): self + { + $clone = clone $this; + $clone->originalPostData = $originalPostData; + + return $clone; + } + + public function withLabels(ParameterBag $labels): self + { + $clone = clone $this; + $clone->labels = $labels; + + return $clone; + } + + public function withSubmitted(ParameterBag $submitted): self + { + $clone = clone $this; + $clone->submitted = $submitted; + + return $clone; + } + + public function withFiles(ParameterBag $files): self + { + $clone = clone $this; + $clone->files = $files; + + return $clone; + } + + public static function create(int $step): self + { + return new self( + $step, + new ParameterBag(), + new ParameterBag(), + new ParameterBag(), + new ParameterBag(), + ); + } +} diff --git a/src/Step/StepDataCollection.php b/src/Step/StepDataCollection.php new file mode 100644 index 0000000..5315608 --- /dev/null +++ b/src/Step/StepDataCollection.php @@ -0,0 +1,73 @@ + + */ + private array $dataPerStep = []; + + public function get(int $step): StepData + { + if (!isset($this->dataPerStep[$step])) { + $this->dataPerStep[$step] = StepData::create($step); + } + + return $this->dataPerStep[$step]; + } + + public function set(StepData $data): self + { + $this->dataPerStep[$data->getStep()] = $data; + + return $this; + } + + public function all(): array + { + return $this->dataPerStep; + } + + public function getAllSubmitted(): array + { + $data = []; + + foreach ($this->all() as $step) { + foreach ($step->getSubmitted()->all() as $k => $v) { + $data[$k] = $v; + } + } + + return $data; + } + + public function getAllLabels(): array + { + $data = []; + + foreach ($this->all() as $step) { + foreach ($step->getLabels()->all() as $k => $v) { + $data[$k] = $v; + } + } + + return $data; + } + + public function getAllFiles(): array + { + $data = []; + + foreach ($this->all() as $step) { + foreach ($step->getFiles()->all() as $k => $v) { + $data[$k] = $v; + } + } + + return $data; + } +} diff --git a/src/Storage/FormManagerAwareInterface.php b/src/Storage/FormManagerAwareInterface.php new file mode 100644 index 0000000..6780bb7 --- /dev/null +++ b/src/Storage/FormManagerAwareInterface.php @@ -0,0 +1,12 @@ +formManager = $formManager; + } +} diff --git a/src/Storage/InMemoryStorage.php b/src/Storage/InMemoryStorage.php new file mode 100644 index 0000000..356598c --- /dev/null +++ b/src/Storage/InMemoryStorage.php @@ -0,0 +1,24 @@ +data[$storageIdentifier] = $stepDataCollection; + } + + public function getData(string $storageIdentifier): StepDataCollection + { + return $this->data[$storageIdentifier] ?? new StepDataCollection(); + } +} diff --git a/src/Storage/SessionReferenceGenerator/FixedSessionReferenceGenerator.php b/src/Storage/SessionReferenceGenerator/FixedSessionReferenceGenerator.php new file mode 100644 index 0000000..61fdd2f --- /dev/null +++ b/src/Storage/SessionReferenceGenerator/FixedSessionReferenceGenerator.php @@ -0,0 +1,19 @@ +identifier; + } +} diff --git a/src/Storage/SessionReferenceGenerator/SessionReferenceGenerator.php b/src/Storage/SessionReferenceGenerator/SessionReferenceGenerator.php new file mode 100644 index 0000000..35aa9a9 --- /dev/null +++ b/src/Storage/SessionReferenceGenerator/SessionReferenceGenerator.php @@ -0,0 +1,15 @@ +writeToSession($storageIdentifier, $stepDataCollection); + } + + public function getData(string $storageIdentifier): StepDataCollection + { + return $this->readFromSession($storageIdentifier); + } + + private function writeToSession(string $storageIdentifier, StepDataCollection $collection): void + { + if (null === ($session = $this->getSession())) { + return; + } + + $session->set($this->getSessionKey($storageIdentifier), $collection); + } + + private function readFromSession(string $storageIdentifier, bool $checkPrevious = false): StepDataCollection + { + $empty = new StepDataCollection(); + + if (null === ($session = $this->getSession($checkPrevious))) { + return $empty; + } + + return $session->get($this->getSessionKey($storageIdentifier), $empty); + } + + private function getSessionKey(string $storageIdentifier): string + { + return self::SESSION_KEY.'.'.$storageIdentifier; + } + + private function getSession(bool $checkPrevious = false): SessionInterface|null + { + if ($checkPrevious && !$this->request->hasPreviousSession()) { + return null; + } + + if (!$this->request->hasSession()) { + return null; + } + + return $this->request->getSession(); + } +} diff --git a/src/Storage/StorageIdentifierGenerator/FixedStorageIdentifierGenerator.php b/src/Storage/StorageIdentifierGenerator/FixedStorageIdentifierGenerator.php new file mode 100644 index 0000000..433f171 --- /dev/null +++ b/src/Storage/StorageIdentifierGenerator/FixedStorageIdentifierGenerator.php @@ -0,0 +1,19 @@ +identifier; + } +} diff --git a/src/Storage/StorageIdentifierGenerator/StorageIdentifierGenerator.php b/src/Storage/StorageIdentifierGenerator/StorageIdentifierGenerator.php new file mode 100644 index 0000000..ee4b5d4 --- /dev/null +++ b/src/Storage/StorageIdentifierGenerator/StorageIdentifierGenerator.php @@ -0,0 +1,24 @@ +getFormId(); + $info[] = $manager->getSessionReference(); + + // Ensure the identifier changes, when the fields are updated as the settings might change + foreach ($manager->getFormFieldModels() as $fieldModel) { + $info[] = $fieldModel->tstamp; + } + + return sha1(implode(';', $info)); + } +} diff --git a/src/Storage/StorageIdentifierGenerator/StorageIdentifierGeneratorInterface.php b/src/Storage/StorageIdentifierGenerator/StorageIdentifierGeneratorInterface.php new file mode 100644 index 0000000..911d2df --- /dev/null +++ b/src/Storage/StorageIdentifierGenerator/StorageIdentifierGeneratorInterface.php @@ -0,0 +1,12 @@ +get('request_stack')->getCurrentRequest(); + + if ($request && System::getContainer()->get('contao.routing.scope_matcher')->isBackendRequest($request)) { + $template = new BackendTemplate('be_wildcard'); + $template->wildcard = '### PAGE BREAK ###'; + + return $template->parse(); + } + + /** @var FormManagerFactoryInterface $factory */ + $factory = System::getContainer()->get(FormManagerFactoryInterface::class); + + $manager = $factory->forFormId((int) $this->pid); + + $this->canGoBack = !$manager->isFirstStep(); + + return parent::parse($attributes); + } + + /** + * Old generate() method that must be implemented due to abstract declaration. + * + * @throws \BadMethodCallException + */ + public function generate(): void + { + throw new \BadMethodCallException('Calling generate() has been deprecated, you must use parse() instead!'); + } +} diff --git a/FormMPFormPlaceholder.php b/src/Widget/Placeholder.php similarity index 58% rename from FormMPFormPlaceholder.php rename to src/Widget/Placeholder.php index 09963bc..57f9540 100644 --- a/FormMPFormPlaceholder.php +++ b/src/Widget/Placeholder.php @@ -1,55 +1,48 @@ - * @license http://opensource.org/licenses/lgpl-3.0.html LGPL - * @link https://github.com/terminal42/contao-mp_forms - */ +declare(strict_types=1); -use Contao\CoreBundle\Exception\ResponseException; -use Contao\Image; -use Contao\Widget; +namespace Terminal42\MultipageFormsBundle\Widget; + +use Codefog\HasteBundle\StringParser; +use Codefog\HasteBundle\UrlParser; use Contao\BackendTemplate; -use Contao\StringUtil as ContaoStringUtil; +use Contao\CoreBundle\Exception\ResponseException; +use Contao\CoreBundle\String\SimpleTokenParser; +use Contao\FrontendTemplate; +use Contao\StringUtil; use Contao\System; -use Haste\Util\StringUtil as HasteStringUtil; -use Haste\Util\Url; +use Contao\Widget; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; +use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\HtmlDumper; -use Symfony\Component\HttpFoundation\File\File; +use Terminal42\MultipageFormsBundle\FormManagerFactoryInterface; -class FormMPFormPlaceholder extends Widget +class Placeholder extends Widget { /** - * Template + * Template. * * @var string */ protected $strTemplate = 'form_mp_form_placeholder'; /** - * The CSS class prefix + * The CSS class prefix. * * @var string */ protected $strPrefix = 'widget widget-placeholder'; /** - * @var boolean + * @var bool */ protected $blnSubmitInput = false; /** - * Do not validate this form field - * - * @param string - * - * @return string + * Do not validate this form field. */ public function validator($input) { @@ -58,14 +51,19 @@ public function validator($input) public function parse($attributes = null) { - if (TL_MODE == 'BE') { + $request = System::getContainer()->get('request_stack')->getCurrentRequest(); + + if ($request && System::getContainer()->get('contao.routing.scope_matcher')->isBackendRequest($request)) { $template = new BackendTemplate('be_wildcard'); $template->wildcard = '### SUMMARY WITH PLACEHOLDERS ###'; return $template->parse(); } - $this->content = ContaoStringUtil::parseSimpleTokens($this->html, $this->generateTokens()); + /** @var SimpleTokenParser $simpleTokenParser */ + $simpleTokenParser = System::getContainer()->get('contao.string.simple_token_parser'); + + $this->content = $simpleTokenParser->parse((string) $this->html, $this->generateTokens()); return parent::parse($attributes); } @@ -75,7 +73,7 @@ public function parse($attributes = null) * * @throws \BadMethodCallException */ - public function generate() + public function generate(): void { throw new \BadMethodCallException('Calling generate() has been deprecated, you must use parse() instead!'); } @@ -83,67 +81,75 @@ public function generate() private function generateTokens(): array { $tokens = []; + $fileTokens = []; $summaryTokens = []; - $manager = new \MPFormsFormManager($this->pid); - $data = $manager->getDataOfAllSteps(); + /** @var FormManagerFactoryInterface $factory */ + $factory = System::getContainer()->get(FormManagerFactoryInterface::class); + + /** @var StringParser $stringParser */ + $stringParser = System::getContainer()->get(StringParser::class); + + /** @var UrlParser $urlParser */ + $urlParser = System::getContainer()->get(UrlParser::class); + + $manager = $factory->forFormId((int) $this->pid); + + $stepsCollection = $manager->getDataOfAllSteps(); - foreach ($data['submitted'] as $k => $v) { - HasteStringUtil::flatten($v, 'form_'.$k, $tokens); + foreach ($stepsCollection->getAllSubmitted() as $k => $v) { + $stringParser->flatten($v, 'form_'.$k, $tokens); $summaryTokens[$k]['value'] = $tokens['form_'.$k]; } - foreach ($data['labels'] as $k => $v) { - HasteStringUtil::flatten($v, 'formlabel_'.$k, $tokens); - + foreach ($stepsCollection->getAllLabels() as $k => $v) { + $stringParser->flatten($v, 'formlabel_'.$k, $tokens); $summaryTokens[$k]['label'] = $tokens['formlabel_'.$k]; } - foreach ($data['files'] as $k => $v) { - $fileTokens = []; - - try{ + foreach ($stepsCollection->getAllFiles() as $k => $v) { + try { $file = new File($v['tmp_name']); } catch (FileNotFoundException $e) { continue; } - if ($k === $_GET['summary_download']) { + if (isset($_GET['summary_download']) && $k === $_GET['summary_download']) { throw new ResponseException(new BinaryFileResponse($file)); } - $fileTokens['download_url'] = Url::addQueryString('summary_download=' .$k); + $fileTokens['download_url'] = $urlParser->addQueryString('summary_download='.$k); $fileTokens['extension'] = $file->getExtension(); $fileTokens['mime'] = $file->getMimeType(); $fileTokens['size'] = $file->getSize(); foreach ($fileTokens as $kk => $vv) { - HasteStringUtil::flatten($vv, 'file_'.$k.'_'.$kk, $tokens); + $stringParser->flatten($vv, 'file_'.$k.'_'.$kk, $tokens); } // Generate a general HTML output using the download template - $tpl = new \Contao\FrontendTemplate('ce_download'); // TODO: make configurable in form field settings? + $tpl = new FrontendTemplate('ce_download'); // TODO: make configurable in form field settings? $tpl->link = $file->getBasename($file->getExtension()); - $tpl->title = ContaoStringUtil::specialchars(sprintf($GLOBALS['TL_LANG']['MSC']['download'], $file->getBasename($file->getExtension()))); + $tpl->title = StringUtil::specialchars(sprintf($GLOBALS['TL_LANG']['MSC']['download'], $file->getBasename($file->getExtension()))); $tpl->href = $fileTokens['download_url']; $tpl->filesize = System::getReadableSize($file->getSize()); $tpl->mime = $file->getMimeType(); $tpl->extension = $file->getExtension(); - HasteStringUtil::flatten($tpl->parse(), 'file_'.$k, $tokens); - + $stringParser->flatten($tpl->parse(), 'file_'.$k, $tokens); $summaryTokens[$k]['value'] = $tokens['file_'.$k]; } // Add a simple summary token that outputs label plus value for everything that was submitted $summaryToken = []; + foreach ($summaryTokens as $k => $v) { - if (!$v['value']) { + if (!isset($v['value'])) { continue; } // Also skip Contao internal tokens and the page switch element - if (in_array($k, ['REQUEST_TOKEN', 'FORM_SUBMIT', 'mp_form_pageswitch'])) { + if (\in_array($k, ['REQUEST_TOKEN', 'FORM_SUBMIT', 'mp_form_pageswitch'], true)) { continue; } @@ -155,13 +161,14 @@ private function generateTokens(): array // Add a debug token to help answering the question "Which tokens are available?" $debugTokens = []; - foreach($tokens as $k => $v) { + + foreach ($tokens as $k => $v) { $debugTokens[sprintf('##%s##', $k)] = $v; } $cloner = new VarCloner(); $dumper = new HtmlDumper(); - $output = fopen('php://memory', 'r+b'); + $output = fopen('php://memory', 'r+'); $dumper->dump($cloner->cloneVar($debugTokens), $output); $output = stream_get_contents($output, -1, 0); diff --git a/tests/AbstractTestCase.php b/tests/AbstractTestCase.php new file mode 100644 index 0000000..57a9133 --- /dev/null +++ b/tests/AbstractTestCase.php @@ -0,0 +1,128 @@ +mockClassWithProperties(FormFieldModel::class, [ + 'id' => 1, + 'pid' => 42, + 'tstamp' => 1, + 'type' => 'text', + ]), + $this->mockClassWithProperties(FormFieldModel::class, [ + 'id' => 2, + 'pid' => 42, + 'tstamp' => 1, + 'type' => 'mp_form_pageswitch', + ]), + $this->mockClassWithProperties(FormFieldModel::class, [ + 'id' => 3, + 'pid' => 42, + 'tstamp' => 1, + 'type' => 'text', + ]), + $this->mockClassWithProperties(FormFieldModel::class, [ + 'id' => 4, + 'pid' => 42, + 'tstamp' => 1, + 'type' => 'mp_form_pageswitch', + ]), + $this->mockClassWithProperties(FormFieldModel::class, [ + 'id' => 5, + 'pid' => 42, + 'tstamp' => 1, + 'type' => 'text', + ]), + $this->mockClassWithProperties(FormFieldModel::class, [ + 'id' => 6, + 'pid' => 42, + 'tstamp' => 1, + 'type' => 'mp_form_pageswitch', + ]), + ]; + + return new Collection($formFieldsModels, 'tl_form_field'); + } + + protected function createStorage(StepDataCollection $initialData = null): InMemoryStorage + { + $storage = new InMemoryStorage(); + + if ($initialData) { + $storage->storeData(self::STORAGE_IDENTIFIER, $initialData); + } + + return $storage; + } + + protected function createFactory(FormModel $formModel, Collection $formFields, StorageInterface $storage, int $step = 0): FormManagerFactory + { + $stack = new RequestStack(); + $request = Request::create('https://www.example.com/form'); + + if ($step) { + $request->query->set('step', $step); + } + + $request->setSession(new Session(new MockArraySessionStorage())); + $stack->push($request); + + $formModelAdapter = $this->mockAdapter(['findByPk']); + $formModelAdapter + ->method('findByPk') + ->willReturn($formModel) + ; + + $formFieldModel = $this->mockAdapter(['findPublishedByPid']); + $formFieldModel + ->method('findPublishedByPid') + ->willReturn($formFields) + ; + + $framework = $this->mockContaoFramework([ + FormModel::class => $formModelAdapter, + FormFieldModel::class => $formFieldModel, + System::class => $this->mockAdapter([]), + Config::class => '', // Not needed in our tests but required for the ContaoTestCase not to fail + ]); + + $factory = new FormManagerFactory( + $framework, + $stack, + new UrlParser() + ); + + $factory->setStorage($storage); + $factory->setStorageIdentifierGenerator(new FixedStorageIdentifierGenerator(self::STORAGE_IDENTIFIER)); + $factory->setSessionReferenceGenerator(new FixedSessionReferenceGenerator(self::SESSION_IDENTIFIER)); + + return $factory; + } +} diff --git a/tests/EventListener/PrepareFormDataListenerTest.php b/tests/EventListener/PrepareFormDataListenerTest.php new file mode 100644 index 0000000..eedb23d --- /dev/null +++ b/tests/EventListener/PrepareFormDataListenerTest.php @@ -0,0 +1,93 @@ +withSubmitted(new ParameterBag(['submitted1' => 'foobar'])); + $stepData = $stepData->withLabels(new ParameterBag(['label1' => 'foobar'])); + $initialData = (new StepDataCollection())->set($stepData); + $storage = $this->createStorage($initialData); + + $form = FormManager::createDummyForm(42); + + $factory = $this->createFactory( + $this->mockClassWithProperties(FormModel::class, ['id' => 42]), + $this->createFormFieldsForValidConfiguration(), + $storage, + 1 // This mocks step=1 (page 2) + ); + + $listener = new PrepareFomDataListener($factory, $this->createMock(RequestStack::class)); + + $submitted = ['submitted2' => 'foobar', 'mp_form_pageswitch' => 'continue']; + $labels = []; + $fields = []; + $files = []; + + try { + $listener($submitted, $labels, $fields, $form, $files); + } catch (RedirectResponseException $exception) { + $this->assertSame( + 'https://www.example.com/form?step=2&ref='.self::SESSION_IDENTIFIER, + $exception->getResponse()->headers->get('Location') + ); + } + + $this->assertSame(['submitted2' => 'foobar', 'mp_form_pageswitch' => 'continue'], $submitted); // "mp_form_pageswitch" should not be removed + $this->assertSame([], $labels); // Test we do not modify the hook parameters if not in last step + + $manager = $factory->forFormId(42); + + $this->assertSame(['submitted1' => 'foobar', 'submitted2' => 'foobar'], $manager->getDataOfAllSteps()->getAllSubmitted()); + $this->assertSame(['label1' => 'foobar'], $manager->getDataOfAllSteps()->getAllLabels()); + } + + public function testDataIsStoredProperlyAndDoesAdjustHookParametersOnLastStep(): void + { + $stepData = StepData::create(0); + $stepData = $stepData->withSubmitted(new ParameterBag(['submitted1' => 'foobar'])); + $stepData = $stepData->withLabels(new ParameterBag(['label1' => 'foobar'])); + $stepData2 = StepData::create(1); + $stepData2 = $stepData2->withSubmitted(new ParameterBag(['submitted2' => 'foobar'])); + $initialData = (new StepDataCollection())->set($stepData)->set($stepData2); + $storage = $this->createStorage($initialData); + + $form = FormManager::createDummyForm(42); + + $factory = $this->createFactory( + $this->mockClassWithProperties(FormModel::class, ['id' => 42]), + $this->createFormFieldsForValidConfiguration(), + $storage, + 2 // This mocks step=2 (page 3 - last page) + ); + + $listener = new PrepareFomDataListener($factory, $this->createMock(RequestStack::class)); + + $submitted = ['submitted3' => 'foobar', 'mp_form_pageswitch' => 'continue']; + $labels = []; + $fields = []; + $files = []; + + $listener($submitted, $labels, $fields, $form, $files); // Must not redirect, so no exception + + // Submitted should now contain all values except for "mp_form_pageswitch" + $this->assertSame(['submitted1' => 'foobar', 'submitted2' => 'foobar', 'submitted3' => 'foobar'], $submitted); + $this->assertSame(['label1' => 'foobar'], $labels); // Test we do not modify the hook parameters if not in last step + } +} diff --git a/tests/FormManagerFactoryTest.php b/tests/FormManagerFactoryTest.php new file mode 100644 index 0000000..0b0cbec --- /dev/null +++ b/tests/FormManagerFactoryTest.php @@ -0,0 +1,90 @@ +push(new Request()); + + $factory = new FormManagerFactory( + $this->createMock(ContaoFramework::class), + $requestStack, + $this->createMock(UrlParser::class) + ); + + $manager = $factory->forFormId(42); + + $this->assertInstanceOf(FormManager::class, $manager); + + $manager2 = $factory->forFormId(42); + + $this->assertSame($manager, $manager2); + } + + public function testCannotCreateManagerIfNoRequest(): void + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Cannot instantiate a FormManager without a request.'); + + $factory = new FormManagerFactory( + $this->createMock(ContaoFramework::class), + new RequestStack(), + $this->createMock(UrlParser::class) + ); + + $factory->forFormId(42); + } + + public function testCustomStorage(): void + { + $requestStack = new RequestStack(); + $requestStack->push(new Request()); + + $storage = new class() implements StorageInterface, FormManagerAwareInterface { + use FormManagerAwareTrait; + + public function getFormManager(): FormManager + { + return $this->formManager; + } + + public function storeData(string $storageIdentifier, StepDataCollection $stepDataCollection): void + { + } + + public function getData(string $storageIdentifier): StepDataCollection + { + return new StepDataCollection(); + } + }; + + $factory = new FormManagerFactory( + $this->createMock(ContaoFramework::class), + $requestStack, + $this->createMock(UrlParser::class) + ); + + $factory->setStorage($storage); + + $manager = $factory->forFormId(42); + + $this->assertSame($manager, $storage->getFormManager()); + } +} diff --git a/tests/Step/StepDataCollectionTest.php b/tests/Step/StepDataCollectionTest.php new file mode 100644 index 0000000..88c512a --- /dev/null +++ b/tests/Step/StepDataCollectionTest.php @@ -0,0 +1,39 @@ +set($this->createStepData(1, new ParameterBag(['foobar' => 'value']))); + $stepCollection->set($this->createStepData(2, new ParameterBag(['foobar_2' => 'value 2']))); + $stepCollection->set($this->createStepData(3, new ParameterBag(['foobar' => 'value 3']))); + + $expected = [ + 'foobar' => 'value 3', + 'foobar_2' => 'value 2', + ]; + + $this->assertSame($expected, $stepCollection->getAllLabels()); + $this->assertSame($expected, $stepCollection->getAllSubmitted()); + $this->assertSame($expected, $stepCollection->getAllFiles()); + } + + private function createStepData(int $step, ParameterBag $parameters): StepData + { + $step = StepData::create($step); + $step = $step->withSubmitted($parameters); + $step = $step->withFiles($parameters); + + return $step->withLabels($parameters); + } +} diff --git a/tests/Step/StepDataTest.php b/tests/Step/StepDataTest.php new file mode 100644 index 0000000..409f8c6 --- /dev/null +++ b/tests/Step/StepDataTest.php @@ -0,0 +1,84 @@ +assertTrue($stepData->getSubmitted()->empty()); + + $stepData = $stepData->withSubmitted($parameters); + $this->assertTrue($parameters->equals($stepData->getSubmitted())); + } + + /** + * @dataProvider parametersDataProvider + */ + public function testFiles(array $data): void + { + $stepData = StepData::create(1); + + $parameters = new ParameterBag($data); + + $this->assertTrue($stepData->getFiles()->empty()); + + $stepData = $stepData->withFiles($parameters); + $this->assertTrue($parameters->equals($stepData->getFiles())); + } + + /** + * @dataProvider parametersDataProvider + */ + public function testOriginalData(array $data): void + { + $stepData = StepData::create(1); + + $parameters = new ParameterBag($data); + + $this->assertTrue($stepData->getOriginalPostData()->empty()); + + $stepData = $stepData->withOriginalPostData($parameters); + $this->assertTrue($parameters->equals($stepData->getOriginalPostData())); + } + + /** + * @dataProvider parametersDataProvider + */ + public function testLabels(array $data): void + { + $stepData = StepData::create(1); + + $parameters = new ParameterBag($data); + + $this->assertTrue($stepData->getLabels()->empty()); + + $stepData = $stepData->withLabels($parameters); + $this->assertTrue($parameters->equals($stepData->getLabels())); + } + + public function parametersDataProvider(): \Generator + { + yield [ + [ + 'value_a' => 'a', + 'value_b' => [ + 'nested' => 'old', + ], + ], + ]; + } +} diff --git a/tools/ecs/composer.json b/tools/ecs/composer.json new file mode 100644 index 0000000..a4dab8b --- /dev/null +++ b/tools/ecs/composer.json @@ -0,0 +1,10 @@ +{ + "require": { + "contao/easy-coding-standard": "^5.4" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + } +} diff --git a/tools/ecs/composer.lock b/tools/ecs/composer.lock new file mode 100644 index 0000000..b1cb97d --- /dev/null +++ b/tools/ecs/composer.lock @@ -0,0 +1,366 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "899ac0b12b774659a830f5070b1c43f1", + "packages": [ + { + "name": "contao/easy-coding-standard", + "version": "5.4.1", + "source": { + "type": "git", + "url": "https://github.com/contao/easy-coding-standard.git", + "reference": "1380515725a3e83f48148ebfc27ce57632978afa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/contao/easy-coding-standard/zipball/1380515725a3e83f48148ebfc27ce57632978afa", + "reference": "1380515725a3e83f48148ebfc27ce57632978afa", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "slevomat/coding-standard": "^7.0 || ^8.0", + "symplify/easy-coding-standard": "^10.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Contao\\EasyCodingStandard\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Leo Feyer", + "homepage": "https://github.com/leofeyer" + } + ], + "description": "EasyCodingStandard configurations for Contao", + "support": { + "issues": "https://github.com/contao/easy-coding-standard/issues", + "source": "https://github.com/contao/easy-coding-standard/tree/5.4.1" + }, + "funding": [ + { + "url": "https://to.contao.org/donate", + "type": "custom" + } + ], + "time": "2022-11-11T08:03:45+00:00" + }, + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v0.7.2", + "source": { + "type": "git", + "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", + "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", + "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "*", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpcompatibility/php-compatibility": "^9.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "franck.nijhof@dealerdirect.com", + "homepage": "http://www.frenck.nl", + "role": "Developer / IT Manager" + }, + { + "name": "Contributors", + "homepage": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "homepage": "http://www.dealerdirect.com", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", + "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + }, + "time": "2022-02-04T12:51:07+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.13.1", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "aac44118344d197e6d5f7c6cee91885f0a89acdd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/aac44118344d197e6d5f7c6cee91885f0a89acdd", + "reference": "aac44118344d197e6d5f7c6cee91885f0a89acdd", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.13.1" + }, + "time": "2022-11-20T08:52:26+00:00" + }, + { + "name": "slevomat/coding-standard", + "version": "8.6.4", + "source": { + "type": "git", + "url": "https://github.com/slevomat/coding-standard.git", + "reference": "8a02c83e59c3230a2a4367b29956a2f2b56e3a24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/8a02c83e59c3230a2a4367b29956a2f2b56e3a24", + "reference": "8a02c83e59c3230a2a4367b29956a2f2b56e3a24", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", + "php": "^7.2 || ^8.0", + "phpstan/phpdoc-parser": ">=1.11.0 <1.14.0", + "squizlabs/php_codesniffer": "^3.7.1" + }, + "require-dev": { + "phing/phing": "2.17.4", + "php-parallel-lint/php-parallel-lint": "1.3.2", + "phpstan/phpstan": "1.4.10|1.9.2", + "phpstan/phpstan-deprecation-rules": "1.0.0", + "phpstan/phpstan-phpunit": "1.0.0|1.2.2", + "phpstan/phpstan-strict-rules": "1.4.4", + "phpunit/phpunit": "7.5.20|8.5.21|9.5.26" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "SlevomatCodingStandard\\": "SlevomatCodingStandard" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "keywords": [ + "dev", + "phpcs" + ], + "support": { + "issues": "https://github.com/slevomat/coding-standard/issues", + "source": "https://github.com/slevomat/coding-standard/tree/8.6.4" + }, + "funding": [ + { + "url": "https://github.com/kukulich", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", + "type": "tidelift" + } + ], + "time": "2022-11-14T09:26:24+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.7.1", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619", + "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards" + ], + "support": { + "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + }, + "time": "2022-06-18T07:21:10+00:00" + }, + { + "name": "symplify/easy-coding-standard", + "version": "10.3.3", + "source": { + "type": "git", + "url": "https://github.com/symplify/easy-coding-standard.git", + "reference": "c93878b3c052321231519b6540e227380f90be17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symplify/easy-coding-standard/zipball/c93878b3c052321231519b6540e227380f90be17", + "reference": "c93878b3c052321231519b6540e227380f90be17", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "conflict": { + "friendsofphp/php-cs-fixer": "<3.0", + "squizlabs/php_codesniffer": "<3.6" + }, + "bin": [ + "bin/ecs" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.3-dev" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Prefixed scoped version of ECS package", + "support": { + "source": "https://github.com/symplify/easy-coding-standard/tree/10.3.3" + }, + "funding": [ + { + "url": "https://www.paypal.me/rectorphp", + "type": "custom" + }, + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2022-06-13T14:03:37+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/tools/ecs/config.php b/tools/ecs/config.php new file mode 100644 index 0000000..de21da8 --- /dev/null +++ b/tools/ecs/config.php @@ -0,0 +1,16 @@ +sets([__DIR__.'/vendor/contao/easy-coding-standard/config/contao.php']); + + $ecsConfig->parallel(); + $ecsConfig->lineEnding("\n"); + + $parameters = $ecsConfig->parameters(); + $parameters->set(Option::CACHE_DIRECTORY, sys_get_temp_dir().'/ecs_default_cache'); +}; \ No newline at end of file diff --git a/tools/phpunit/composer.json b/tools/phpunit/composer.json new file mode 100644 index 0000000..67e74db --- /dev/null +++ b/tools/phpunit/composer.json @@ -0,0 +1,6 @@ +{ + "require": { + "phpunit/phpunit": "^9.5", + "contao/test-case": "^5.0" + } +} diff --git a/tools/phpunit/composer.lock b/tools/phpunit/composer.lock new file mode 100644 index 0000000..cee39d7 --- /dev/null +++ b/tools/phpunit/composer.lock @@ -0,0 +1,1807 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "dcbe44d954a6efea212d55f3e71f40e2", + "packages": [ + { + "name": "contao/test-case", + "version": "5.0.7", + "source": { + "type": "git", + "url": "https://github.com/contao/test-case.git", + "reference": "10fcffa6237ab6cc6787430565af3ba9696938cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/contao/test-case/zipball/10fcffa6237ab6cc6787430565af3ba9696938cf", + "reference": "10fcffa6237ab6cc6787430565af3ba9696938cf", + "shasum": "" + }, + "require": { + "php": "^8.1", + "phpunit/phpunit": "^9.5" + }, + "conflict": { + "phpunit/phpunit": "<8.0", + "roave/better-reflection": "<4.12.2 || >=6.0" + }, + "require-dev": { + "contao/core-bundle": "self.version", + "doctrine/dbal": "^3.3", + "doctrine/orm": "^2.10", + "ext-pdo": "*", + "symfony/http-client": "^5.4 || ^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Contao\\TestCase\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Leo Feyer", + "homepage": "https://github.com/leofeyer" + } + ], + "description": "Contao 4 test case", + "support": { + "issues": "https://github.com/contao/test-case/issues", + "source": "https://github.com/contao/test-case/tree/5.0.7" + }, + "funding": [ + { + "url": "https://to.contao.org/donate", + "type": "custom" + } + ], + "time": "2022-11-14T09:51:34+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", + "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.22" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.4.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-03-03T08:28:38+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2022-03-03T13:19:32+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.15.2", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", + "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.2" + }, + "time": "2022-11-12T15:38:23+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.3" + }, + "time": "2021-07-20T11:28:43+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.21", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "3f893e19712bb0c8bc86665d1562e9fd509c4ef0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/3f893e19712bb0c8bc86665d1562e9fd509c4ef0", + "reference": "3f893e19712bb0c8bc86665d1562e9fd509c4ef0", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.14", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "*", + "ext-xdebug": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.21" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-12-14T13:26:54+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.5.27", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "a2bc7ffdca99f92d959b3f2270529334030bba38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a2bc7ffdca99f92d959b3f2270529334030bba38", + "reference": "a2bc7ffdca99f92d959b3f2270529334030bba38", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.13", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.5", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^3.2", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "*", + "ext-xdebug": "*" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.27" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2022-12-09T07:31:23+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:08:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.7", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:52:27+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:10:38+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7", + "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-04-03T09:37:03+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T06:03:37+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-02-14T08:28:10+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.6", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-28T06:42:11+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:17:30+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:45:17+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", + "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-12T14:47:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2021-07-28T10:34:58+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/tools/phpunit/phpunit.xml.dist b/tools/phpunit/phpunit.xml.dist new file mode 100644 index 0000000..d105cd9 --- /dev/null +++ b/tools/phpunit/phpunit.xml.dist @@ -0,0 +1,18 @@ + + + + + + + + + + ../../tests + + + \ No newline at end of file