<?php

namespace UncleCheese\BetterButtons\Buttons;

use SilverStripe\View\Requirements;
use UncleCheese\BetterButtons\Buttons\BetterButton;

/**
 * Defines the button that deletes a record, with confirmation
 *
 * @author  Uncle Cheese <unclecheese@leftandmain.com>
 * @package  silverstripe-gridfield-betterbuttons
 */
class BetterButton_Delete extends BetterButton
{
    /**
     * Builds the button
     */
    public function __construct()
    {
        parent::__construct('doDelete', _t('GridFieldDetailForm.Delete', 'Delete'));
    }

    /**
     * Adds the JS, sets up necessary HTML attributes
     * @return FormAction
     */
    public function baseTransform()
    {
        parent::baseTransform();
        Requirements::javascript(BETTER_BUTTONS_DIR.'/javascript/gridfield_betterbuttons_delete.js');

        return $this
            ->setUseButtonTag(true)
            ->addExtraClass('btn-danger-outline btn-hide-outline font-icon-trash-bin gridfield-better-buttons-delete')
            ->setAttribute("data-toggletext", _t('GridFieldBetterButtons.AREYOUSURE', 'Yes. Delete this item.'))
            ->setAttribute("data-confirmtext", _t('GridFieldDetailForm.CANCELDELETE', 'No. Don\'t delete.'));
    }

    /**
     * Determines if the button should show
     * @return boolean
     */
    public function shouldDisplay()
    {
        return !$this->gridFieldRequest->recordIsPublished() && $this->gridFieldRequest->record->canDelete();
    }
}


<?php

namespace UncleCheese\BetterButtons\Controllers;

use Exception;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField;
use SilverStripe\View\Requirements;
use UncleCheese\BetterButtons\Actions\BetterButtonNestedForm;
use UncleCheese\BetterButtons\Controllers\BetterButtonsCustomActionRequest;

/**
 * Request handler that deals with nested forms
 *
 * @author  Uncle Cheese <unclecheese@leftandmain.com>
 * @package  silverstripe-gridfield-betterbuttons
 */
class BetterButtonsNestedFormRequest extends BetterButtonsCustomActionRequest
{
    /**
     * Define the allowed controller actions
     * @var array
     */
    private static $allowed_actions = array(
        'Form'
    );

    /**
     * Define URL routes
     * @var array
     */
    private static $url_handlers = array(
        'Form' => 'Form'
    );

    /**
     * Gets a link to this RequestHandler
     */
    public function Link($action = null)
    {
        return $this->controller->Link('nestedform');
    }

    /**
     * Create the nested form
     *
     * @return  Form
     */
    public function Form()
    {
        $formAction = $this->getFormActionFromRequest($this->request);
        $fields = $formAction->getFields();
        $fields->push(HiddenField::create('action', '', $formAction->getButtonName()));

        $form = Form::create(
            $this,
            'Form',
            $fields,
            FieldList::create(
                FormAction::create('nestedFormSave', 'Save')
            )
        );

        return $form;
    }

    /**
     * Render the form to the template
     * @param  HTTPRequest $r
     * @return SSViewer
     */
    public function index(HTTPRequest $r)
    {
        Requirements::css(BETTER_BUTTONS_DIR.'/css/betterbuttons_nested_form.css');

        return $this->customise(array(
            'Form' => $this->Form()
        ))->renderWith(BetterButtonNestedForm::class);
    }

    /**
     * Handles the saving of the nested form. This is essentially a proxy method
     * for the method that the BetterButtonNestedForm button has been configured
     * to use
     *
     * @param  array $data    The form data
     * @param  Form $form     The nested form object
     * @param  HTTPRequest $request
     * @return HTTPResponse
     */
    public function nestedFormSave($data, $form, $request)
    {
        $formAction = $this->getFormActionFromRequest($request);
        $actionName = $formAction->getButtonName();

        $this->record->$actionName($data, $form, $request);

        return Controller::curr()->redirectBack();
    }

    /**
     * Get the action from the request, whether it's part of the form data
     * or in the query string
     *
     * @param  HTTPRequest $r
     * @return BetterButtonNestedForm
     * @throws Exception If the action doesn't exist, or isn't a BetterButtonNestedForm
     */
    protected function getFormActionFromRequest(HTTPRequest $r)
    {
        $action = $r->requestVar('action');
        $formAction = $this->record->findActionByName($action);

        if (!$formAction instanceof BetterButtonNestedForm) {
            throw new Exception("Action $action doesn't exist or is not a BetterButtonNestedForm");
        }

        return $formAction;
    }
}


<?php

namespace UncleCheese\BetterButtons\Buttons;

use UncleCheese\BetterButtons\Buttons\BetterButton;

/**
 * Defines the button that saves a record and goes to the previous one
 *
 * @author  Uncle Cheese <unclecheese@leftandmain.com>
 * @package  silverstripe-gridfield-betterbuttons
 */
class BetterButton_SaveAndPrev extends BetterButton
{
    /**
     * Builds the button
     */
    public function __construct()
    {
        parent::__construct("doSaveAndPrev", _t('GridFieldDetailForm.SAVEANDPREV', 'Save and go to previous record'));
    }

    /**
     * Determines if the button should show
     * @return boolean
     */
    public function shouldDisplay()
    {
        $record = $this->gridFieldRequest->record;

        return $record->canEdit();
    }

    /**
     * Updates the button to be disabled if there is no previous record
     * @return FormAction
     */
    public function baseTransform()
    {
        parent::baseTransform();
        $disabled = (!$this->gridFieldRequest->getPreviousRecordID());

        return $this->setDisabled($disabled);
    }

    /**
     * Adds a class to help identify the button in a group
     * @return FormAction
     */
    public function transformToInput()
    {
        return parent::transformToInput()
            ->addExtraClass("saveAndGoPrev");
    }
}


<?php

namespace UncleCheese\BetterButtons\Buttons;

use UncleCheese\BetterButtons\Buttons\BetterButton;

/**
 * Defines the button that saves a record and goes to the next one
 *
 * @author  Uncle Cheese <unclecheese@leftandmain.com>
 * @package  silverstripe-gridfield-betterbuttons
 */
class BetterButton_SaveAndNext extends BetterButton
{
    /**
     * Builds the button
     */
    public function __construct()
    {
        parent::__construct("doSaveAndNext", _t('GridFieldDetailForm.SAVEANDNEXT', 'Save and go to next record'));
    }

    /**
     * Determines if the button should display
     * @return boolean
     */
    public function shouldDisplay()
    {
        $record = $this->gridFieldRequest->record;

        return $record->canEdit();
    }

    /**
     * Disables the button if there is no next record
     * @return FormAction
     */
    public function baseTransform()
    {
        parent::baseTransform();
        $disabled = (!$this->gridFieldRequest->getNextRecordID());

        return $this->setDisabled($disabled);
    }

    /**
     * Adds a class that helps identify this button in a group
     * @return FormAction
     */
    public function transformToInput()
    {
        return parent::transformToInput()
            ->addExtraClass("saveAndGoNext");
    }
}


<?php

namespace UncleCheese\BetterButtons\Extensions;

use Exception;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Forms\CompositeField;
use SilverStripe\Forms\FieldList;
use SilverStripe\ORM\DataExtension;
use SilverStripe\Versioned\Versioned;
use UncleCheese\BetterButtons\Buttons\BetterButton;
use UncleCheese\BetterButtons\FormFields\DropdownFormAction;

/**
 * An extension that offers features to DataObjects that allow them to set their own
 * actions and utilities for {@link GridFieldDetailForm}
 *
 * Default buttons are defined in _config.yml and can be overriden via the Config layer.
 * Due to the way Config merges arrays, set button class names to "false" to remove them from the list.
 *
 * @author  Uncle Cheese <unclecheese@leftandmain.com>
 * @package  silverstripe-gridfield-betterbuttons
 */
class BetterButtonDataObject extends DataExtension
{
    /**
     * Enable better buttons for this DataObject
     *
     * @config
     * @var bool
     */
    private static $better_buttons_enabled = true;

    /**
     * Enable versioned controls like 'Save & Publish' for DataObjects
     * with 'Versioned' extension.
     *
     * Disable this for records where you want a parent DataObject to control the
     * published/unpublished state of its children. ie. User Defined Forms 3.0+.
     *
     * @config
     * @var bool
     */
    private static $better_buttons_versioned_enabled = true;

    /**
     * Gets the default actions for all DataObjects. Can be overloaded in subclasses
     *
     *  public function getBetterButtonsActions()
     *  {
     *      $actions = parent::getBetterButtonsActions();
     *      $actions->push(BetterButtonCustomAction::create('myaction','Do something to this record'));
     *
     *      return $actions;
     *  }
     *
     *
     * @return FieldList
     */
    public function getBetterButtonsActions()
    {
        $buttons = $this->getDefaultButtonList("BetterButtonsActions");
        $actions = $this->createFieldList($buttons);

        $this->owner->extend('updateBetterButtonsActions', $actions);

        return $actions;
    }

    /**
     * Gets a FormAction or BetterButtonCustomAction by name, in utils or actions
     * @param  string $action  The name of the action to find
     * @return FormAction
     */
    public function findActionByName($action)
    {
        $actions = $this->owner->getBetterButtonsActions();
        $formAction = false;

        foreach ($actions as $f) {
            if ($formAction) {
                break;
            }

            if ($f instanceof CompositeField) {
                $formAction = $f->fieldByName($action);
            } elseif ($f->getName() === $action) {
                $formAction = $f;
            }
        }

        if (!$formAction) {
            $utils = $this->owner->getBetterButtonsUtils();
            $formAction = $utils->fieldByName($action);
        }

        return $formAction;
    }

    /**
     * Gets the default utils for all DataObjects. Can be overloaded in subclasses.
     * Utils are actions that appear in the top of the GridFieldDetailForm
     *
     *  public function getBetterButtonsUtils()
     *  {
     *      $utils = parent::getBetterButtonsUtils();
     *      $utils->push(BetterButtonCustomAction::create('myaction','Do something to this record'));
     *
     *      return $utils;
     *  }
     *
     *
     * @return FieldList
     */
    public function getBetterButtonsUtils()
    {
        $buttons = $this->getDefaultButtonList("BetterButtonsUtils");
        $utils = $this->createFieldList($buttons);

        $this->owner->extend('updateBetterButtonsUtils', $utils);

        return $utils;
    }

    /**
     * Gets an array of all the default buttons as defined in the config
     * @param  array $config
     * @return array
     */
    protected function getDefaultButtonList($config)
    {
        $new = ($this->owner->ID == 0);
        $list = $new
            ? Config::inst()->get($config, $this->checkVersioned() ? "versioned_create" : "create")
            : Config::inst()->get($config, $this->checkVersioned() ? "versioned_edit" : "edit");

        return $list ?: array ();
    }

    /**
     * Transforms a list of configured buttons into a usable FieldList
     * @param  array                            $buttons An array of class names
     * @return FieldList
     */
    protected function createFieldList($buttons)
    {
        $actions = FieldList::create();
        foreach ($buttons as $buttonType => $bool) {
            if (!$bool || !$buttonType) {
                continue;
            }

            if (substr($buttonType, 0, 6) == "Group_") {
                $group = $this->createButtonGroup(substr($buttonType, 6));
                if ($group->children->exists()) {
                    $actions->push($group);
                }
            } elseif ($b = $this->instantiateButton($buttonType)) {
                $actions->push($b);
            }
        }

        return $actions;
    }

    /**
     * Transforms a given button class name into an actual object.
     * @param  string                           $className The class of the button
     * @param  Form                             $form      The form that will contain the button
     * @param  GridFieldDetailForm_ItemRequest  $request   The request that points to the form
     * @param  boolean                          $button    If the button should display as an input tag or a button
     * @return BetterButton
     * @throws Exception If the requested button type does not exist
     */
    protected function instantiateButton($className)
    {
        try {
            return Injector::inst()->create($className);
        } catch (Exception $ex) {
            // Customize the default injector exception
            throw new Exception("The button type $className doesn't exist.");
        }
    }

    /**
     * Creates a button group {@link DropdownFormAction}
     * @param  string                           $groupName The name of the group
     * @return DropdownFormAction
     */
    protected function createButtonGroup($groupName)
    {
        $groupConfig = Config::inst()->get("BetterButtonsGroups", $groupName);
        $label = (isset($groupConfig['label'])) ? $groupConfig['label'] : $groupName;
        $buttons = (isset($groupConfig['buttons'])) ? $groupConfig['buttons'] : array ();
        $button = DropdownFormAction::create(_t('GridFieldBetterButtons.'.$groupName, $label));
        foreach ($buttons as $b => $bool) {
            if ($bool) {
                if ($child = $this->instantiateButton($b)) {
                    $button->push($child);
                }
            }
        }

        return $button;
    }

    /**
     * Determines if the record is using the {@link Versioned} extension
     * @return boolean
     */
    public function checkVersioned()
    {
        $isVersioned = false;

        foreach ($this->owner->getExtensionInstances() as $extension) {
            if ($extension instanceof Versioned) {
                $isVersioned = true;
                break;
            }
        }

        return $isVersioned
            && $this->owner->config()->better_buttons_versioned_enabled
            && count($this->owner->getVersionedStages()) > 1;
    }

    /**
     * Checks if a custom action is allowed to be called against a model.
     * Prevents security risk of calling arbitrary public methods on the DataObject.
     *
     * Looks at:
     *
     *     private static $better_buttons_actions = array ()
     *
     *
     * @param  string  $action The name of the action
     * @return boolean
     */
    public function isCustomActionAllowed($action)
    {
        $actions = $this->owner->config()->better_buttons_actions;
        if ($actions) {
            return in_array($action, $actions);
        }

        return false;
    }
}


<?php

namespace UncleCheese\BetterButtons\Buttons;

use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest;
use UncleCheese\BetterButtons\Extensions\BetterButtonGroupable;
use UncleCheese\BetterButtons\Interfaces\BetterButtonInterface;

/**
 * Defines the base class for all buttons that submit form data through GridField
 *
 * @author  Uncle Cheese <unclecheese@leftandmain.com>
 * @package  silverstripe-gridfield-betterbuttons
 */
abstract class BetterButton extends FormAction implements BetterButtonInterface
{
    private static $extensions = array (
        BetterButtonGroupable::class
    );

    /**
     * The request that points to the GridFieldDetailForm
     * @var GridFieldDetailForm_ItemRequest
     */
    protected $gridFieldRequest;

    /**
     * Bind to the GridField request
     * @param Form $form
     * @param GridFieldDetailForm_ItemRequest $request
     */
    public function bindGridField(Form $form, GridFieldDetailForm_ItemRequest $request)
    {
        $this->setForm($form);
        $this->gridFieldRequest = $request;

        return $this;
    }

    /**
     * Performs any last-minute transformations to the button in accordance with anything
     * the user may have done after instantiating the button
     *
     * @return BetterButton
     */
    public function baseTransform()
    {
        return $this;
    }

    /**
     * Tells the form action to become a standard form submit button
     * @return FormAction
     */
    public function transformToButton()
    {
        $this->baseTransform();

        return $this->setUseButtonTag(true);
    }

    /**
     * Tells the form action to become a standard input tag, e.g. for usage in a button group
     * @return FormAction
     */
    public function transformToInput()
    {
        $this->baseTransform();

        return $this;
    }

    /**
     * Determines if the button should display or not
     * @return bool
     */
    public function shouldDisplay()
    {
        return true;
    }

    /**
     * Render the field with the correct attributes
     * @param array $properties
     * @return  FormAction
     */
    public function Field($properties = array ())
    {
        if ($this->isGrouped()) {
            $this->transformToInput();
        } else {
            $this->transformToButton();
        }

        return parent::Field($properties);
    }
}


<?php

namespace UncleCheese\BetterButtons\Buttons;

use UncleCheese\BetterButtons\Buttons\BetterButton;

/**
 * Defines the button that creates a new record
 *
 * @author  Uncle Cheese <unclecheese@leftandmain.com>
 * @package  silverstripe-gridfield-betterbuttons
 */
class BetterButton_New extends BetterButton
{
    /**
     * Builds the button
     * @param Form                            $form    The form that holds the button
     * @param GridFieldDetailForm_ItemRequest $request The request that points to the form
     */
    public function __construct()
    {
        parent::__construct("doNew", _t('GridFieldBetterButtons.NEWRECORD', 'New record'));
    }

    /**
     * Add the necessary classes and icons
     * @return FormAction
     */
    public function baseTransform()
    {
        parent::baseTransform();

        return $this->addExtraClass("btn-primary font-icon-plus ss-ui-action-constructive");
    }

    /**
     * Determines if the button should show
     * @return boolean
     */
    public function shouldDisplay()
    {
         // Do not show create new within create new
        if ($this->gridFieldRequest->getRequest()->param('ID') == 'new') {
            return false;
        }
        return $this->gridFieldRequest->record->canCreate();
    }
}


<?php

namespace UncleCheese\BetterButtons\Buttons;

use UncleCheese\BetterButtons\Buttons\BetterButton;
use UncleCheese\BetterButtons\Interfaces\BetterButton_Versioned;

/**
 * Defines the button that rolls back the version of the record
 *
 * @author  Uncle Cheese <unclecheese@leftandmain.com>
 * @package  silverstripe-gridfield-betterbuttons
 */
class BetterButton_Rollback extends BetterButton implements BetterButton_Versioned
{
    /**
     * Builds the button
     */
    public function __construct()
    {
        parent::__construct('rollback', _t('SiteTree.BUTTONCANCELDRAFT', 'Cancel draft changes'));
    }

    /**
     * Update the button to show a description
     * @return [type] [description]
     */
    public function baseTransform()
    {
        parent::baseTransform();

        return $this->setDescription(_t(
            'SiteTree.BUTTONCANCELDRAFTDESC',
            'Delete your draft and revert to the currently published page'
        ));
    }

    /**
     * Determines if the button should display
     * @return boolean
     */
    public function shouldDisplay()
    {
        return $this->gridFieldRequest->record->stagesDiffer('Stage', 'Live')
            && $this->gridFieldRequest->recordIsPublished()
            && $this->gridFieldRequest->record->canEdit();
    }
}


<?php

namespace UncleCheese\BetterButtons\Actions;

use Exception;
use SilverStripe\Control\Controller;
use UncleCheese\BetterButtons\Actions\BetterButtonAction;

/**
 * Defines an arbitrary action that can be taken from a grid field detail form
 *
 * @author  Uncle Cheese <unclecheese@leftandmain.com>
 * @package  silverstripe-gridfield-better-buttons
 */
class BetterButtonCustomAction extends BetterButtonAction
{
    /**
     * @constant A symbol representing the "go back" behaviour
     */
    const GOBACK = 1;

    /**
     * @constant A symbol representing the "refresh" behaviour
     */
    const REFRESH = 2;

    /**
     * The name of the action (e.g. a method) to take on the model
     * @var string
     */
    protected $actionName;

    /**
     * The type of redirect, see GOBACK and REFRESH constants
     * @var int
     */
    protected $redirectType;

    /**
     * The redirect URL. Overrides $redirectType
     *
     * @var string
     */
    protected $redirectURL;

    /**
     * Builds the button
     * @param string $actionName   The name of the action (method)
     * @param string $text         The text for the button
     * @param int    $redirectType The type of redirection on completion of the action
     */
    public function __construct($actionName, $text, $redirectType = null)
    {
        $this->actionName = $actionName;
        $this->redirectType = $redirectType ?: self::REFRESH;

        parent::__construct($text);
    }

    /**
     * Button name in this case has to be predictable so we can find it in a set
     * to call a custom action
     *
     * @return string
     */
    public function getButtonName()
    {
        return $this->actionName;
    }

    /**
     * Sets the behaviour on completion of the action, either refresh or go back to list.
     * See self::GOBACK and self::REFRESH constants
     *
     * @param int $type
     * @throws Exception if the redirect type is not supported
     */
    public function setRedirectType($type)
    {
        if (!in_array($type, array(self::GOBACK, self::REFRESH))) {
            throw new Exception(
                "Redirect type must use either the GOBACK or REFRESH constants on BetterButtonCustomAction"
            );
        }

        $this->redirectType = $type;

        return $this;
    }

    /**
     * Gets the redirect type
     * @return int
     */
    public function getRedirectType()
    {
        return $this->redirectType;
    }

    /**
     * Sets the redirect URL. Overrides $redirectType;
     *
     * @param string $url
     */
    public function setRedirectURL($url)
    {
        $this->redirectURL = $url;

        return $this;
    }

    /**
     * Gets the redirect URL
     * @return string
     */
    public function getRedirectURL()
    {
        return $this->redirectURL;
    }

    /**
     * Gets the link for the button
     * @return string
     */
    public function getButtonLink()
    {
        $link = Controller::join_links(
            'customaction',
            $this->actionName
        );
        return $this->gridFieldRequest->Link($link);
    }
}


<?php

namespace UncleCheese\BetterButtons\Buttons;

use UncleCheese\BetterButtons\Buttons\BetterButton;
use UncleCheese\BetterButtons\Interfaces\BetterButton_Versioned;

/**
 * Defines the button that publishes a record that uses the {@link Versioned} extension
 *
 * @author  Uncle Cheese <unclecheese@leftandmain.com>
 * @package  silverstripe-gridfield-betterbuttons
 */
class BetterButton_Publish extends BetterButton implements BetterButton_Versioned
{
    /**
     * Builds the button
     */
    public function __construct()
    {
        parent::__construct('publish', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'));
    }

    /**
     * Determines if the button should display
     * @return boolean
     */
    public function shouldDisplay()
    {
        $record = $this->gridFieldRequest->record;

        return $record->canEdit();
    }

    /**
     * Updates the button to use appropriate icons
     * @return FormAction
     */
    public function baseTransform()
    {
        parent::baseTransform();
        return $this
            ->addExtraClass('btn-secondary-outline font-icon-check-mark')
            ->setAttribute('data-btn-alternate', 'btn action btn-primary font-icon-rocket')
            ->setAttribute('data-text-alternate', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'));
    }

    /**
     * Update the UI to reflect published state
     * @return void
     */
    public function transformToButton()
    {
        parent::transformToButton();

        if ($this->gridFieldRequest->recordIsPublished()) {
            $this->setTitle(_t('SiteTree.BUTTONPUBLISHED', 'Published'));
        }

        if ($this->gridFieldRequest->record->stagesDiffer('Stage', 'Live')
            && $this->gridFieldRequest->recordIsDeletedFromStage()
        ) {
            $this->addExtraClass('ss-ui-alternate');
        }

        return $this;
    }
}


<?php

namespace UncleCheese\BetterButtons\Actions;

use SilverStripe\Core\Convert;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest;
use SilverStripe\Forms\LiteralField;
use UncleCheese\BetterButtons\Extensions\BetterButtonGroupable;
use UncleCheese\BetterButtons\Interfaces\BetterButtonInterface;

/**
 * Defines the base class for all buttons that link to arbitrary endpoints
 * from a {@link GridFieldDetailForm}
 *
 * @author  Uncle Cheese <unclecheese@leftandmain.com>
 * @package  silverstripe-gridfield-betterbuttons
 */
class BetterButtonAction extends LiteralField implements BetterButtonInterface
{
    private static $extensions = array(
        BetterButtonGroupable::class
    );

    /**
     * The form that this action is associated with
     * @var Form
     */
    protected $form;

    /**
     * The text to appear on the button
     * @var string
     */
    protected $buttonText;

    /**
     * The name of the button, used just for uniqueness
     * @var string
     */
    protected $buttonName;

    /**
     * The request that is associated with the gridfield
     * @var GridFieldDetailForm_ItemRequest
     */
    protected $gridFieldRequest;

    /**
     * Builds the action
     * @param string $text The text to appear on the button
     */
    public function __construct($text = null)
    {
        $this->buttonText = $text;
        parent::__construct($this->getButtonName(), "");
    }

    /**
     * Bind the button to the GridField request
     * @param Form $form
     * @param GridFieldDetailForm_ItemRequest $request
     */
    public function bindGridField(Form $form, GridFieldDetailForm_ItemRequest $request)
    {
        $this->setForm($form);
        $this->gridFieldRequest = $request;

        return $this;
    }

    /**
     * Get the name of the button. Arbitrary.
     * @return string
     */
    public function getButtonName()
    {
        $raw = $this->buttonName ?: $this->getButtonText();

        return preg_replace('/[^a-z0-9-_]/', '', strtolower($this->getButtonText()));
    }

    /**
     * A noop that gets the link for the button
     * @return string
     */
    public function getButtonLink()
    {
    }

    /**
     * Determines if the button should display
     * @return bool
     */
    public function shouldDisplay()
    {
        return true;
    }

    /**
     * Gets the HTML representing the button
     * @return string
     */
    public function getButtonHTML()
    {
        return sprintf(
            '%s',
            $this->getButtonClasses(),
            $this->getButtonLink(),
            // Prevent outputting the 'class' attribute twice by excluding it from other attributes
            $this->getAttributesHTML('class'),
            $this->getButtonText()
        );
    }

    /**
     * Combines classes from $this->extraClass() with a couple of additional classes if they are
     * applicable for this button.
     * @return string
     */
    private function getButtonClasses()
    {
        $classes = $this->extraClass();
        if ($this->isGrouped()) {
            return $classes; //Do not return the below additional classes
        }
        $classes .= ' btn btn-default ss-ui-button';
        if ($this->getAttribute('target') != '_blank') {
            // Only add this class if this link is targeted inside the CMS. Any links targeted to a new browser
            // window/tab should not have this as the CMS JavaScript would hook to the 'onclick' event and load the
            // content via AJAX to the CMS, which could cause problems.
            $classes .= ' cms-panel-link';
        }

        return $classes;
    }

    /**
     * Gets the text for the button
     * @return string
     */
    public function getButtonText()
    {
        return $this->buttonText;
    }

    /**
     * Sets the confirm text
     * @param  $str
     * @return  BetterButtonAction
     */
    public function setConfirmation($str)
    {
        return $this->setAttribute('data-confirm', Convert::raw2att($str));
    }

    /**
     * Generates the button. Updates the literal field with the correct HTML
     * based on any post-contruct updates
     *
     * @param array $attributes
     * @return SSViewer
     */
    public function FieldHolder($attributes = array ())
    {
        if ($this->shouldDisplay()) {
            $this->setContent($this->getButtonHTML());
            return parent::FieldHolder($attributes);
        }
    }
}


<?php

namespace UncleCheese\BetterButtons\Extensions;

use Exception;
use SilverStripe\Admin\LeftAndMain;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\PjaxResponseNegotiator;
use SilverStripe\Core\Convert;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest;
use SilverStripe\Forms\TabSet;
use SilverStripe\ORM\DataExtension;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataModel;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\ManyManyList;
use SilverStripe\ORM\ValidationException;
use SilverStripe\Versioned\Versioned;
use SilverStripe\View\Requirements;
use UncleCheese\BetterButtons\Controllers\BetterButtonsCustomActionRequest;
use UncleCheese\BetterButtons\Controllers\BetterButtonsNestedFormRequest;
use UncleCheese\BetterButtons\Interfaces\BetterButtonInterface;
use UncleCheese\BetterButtons\Interfaces\BetterButton_Versioned;

/**
 * Decorates {@link GridDetailForm_ItemRequest} to use new form actions and buttons.
 *
 * @author  Uncle Cheese <unclecheese@leftandmain.com>
 * @package  silverstripe-gridfield-betterbuttons
 */
class GridFieldBetterButtonsItemRequest extends DataExtension
{
    /**
     * @var array Allowed controller actions
     */
    private static $allowed_actions = array(
        'addnew',
        'edit',
        'save',
        'cancel',
        'publish',
        'rollback',
        'unpublish',
        'ItemEditForm',
        'doNew',
        'doSaveAndAdd',
        'doSaveAndQuit',
        'doPublishAndAdd',
        'doPublishAndClose',
        'doSaveAndNext',
        'doSaveAndPrev',
        'doDelete',
        'customaction',
        'nestedform',
    );

    /**
     * Handles all custom action from DataObjects and hands them off to a sub-controller.
     * e.g. /customaction/mymethodname
     *
     * Can't handle the actions here because the url_param '$Action!' gets matched, and we don't
     * get to read anything after /customaction/
     *
     * @param  HTTPRequest $r
     * @return BetterButtonsCustomActionRequest
     */
    public function customaction(HTTPRequest $r)
    {
        $req = new BetterButtonsCustomActionRequest($this, $this->owner, $this->owner->ItemEditForm());

        return $req->handleRequest($r, DataModel::inst());
    }

    /**
     * Handles all custom action from DataObjects and hands them off to a sub-controller.
     * e.g. /nestedform?action=myDataObjectAction
     *
     * @param  HTTPRequest $r
     * @return BetterButtonsNestedFormRequest
     */
    public function nestedform(HTTPRequest $r)
    {
        $req = new BetterButtonsNestedFormRequest($this, $this->owner, $this->owner->ItemEditForm());

        return $req->handleRequest($r, DataModel::inst());
    }

    /**
     * Redirecting to the current URL doesn't do anything, so this is just a dummy action
     * that gives the request somewhere to go in order to force a reload, and then just
     * redirects back to the original link.
     *
     * @param HTTPRequest The request object
     */
    public function addnew(HTTPRequest $r)
    {
        return Controller::curr()->redirect(Controller::join_links($this->owner->gridField->Link("item"), "new"));
    }

    /**
     * Updates the detail form to include new form actions and buttons
     *
     * @param Form The ItemEditForm object
     */
    public function updateItemEditForm($form)
    {
        if ($this->owner->record->stat('better_buttons_enabled') !== true) {
            return false;
        }
        Requirements::css(BETTER_BUTTONS_DIR.'/css/gridfield_betterbuttons.css');
        Requirements::javascript(BETTER_BUTTONS_DIR.'/javascript/gridfield_betterbuttons.js');

        $actions = $this->owner->record->getBetterButtonsActions();
        $form->setActions($this->filterFieldList($form, $actions));

        if ($form->Fields()->hasTabSet()) {
            $form->Fields()->findOrMakeTab('Root')->setTemplate(TabSet::class);
            $form->addExtraClass('cms-tabset');
        }

        $utils = $this->owner->record->getBetterButtonsUtils();
        $form->Utils = $this->filterFieldList($form, $utils);
        $form->setTemplate([
            'type' => 'Includes',
            'BetterButtons_EditForm',
        ]);
        $form->addExtraClass('better-buttons-form');
    }

    /**
     * Given a list of actions, remove anything that doesn't belong.
     * @param  Form      $form
     * @param  FieldList $actions
     * @return FieldList
     */
    protected function filterFieldList(Form $form, FieldList $actions)
    {
        $list = FieldList::create();

        foreach ($actions as $a) {
            if (!$a instanceof BetterButtonInterface) {
                throw new Exception("{$buttonObj->class} must implement BetterButtonInterface");
            }

            $a->bindGridField($form, $this->owner);

            if (!$a->shouldDisplay()) {
                continue;
            }

            if (($a instanceof BetterButton_Versioned) && !$this->owner->record->checkVersioned()) {
                continue;
            }

            $list->push($a);
        }

        return $list;
    }

    /**
     * Saves the form and forwards to a blank form to continue creating
     *
     * @param array The form data
     * @param Form The form object
     */
    public function doSaveAndAdd($data, $form)
    {
        return $this->saveAndRedirect($data, $form, $this->owner->Link("addnew"));
    }

    /**
     * Saves the form and goes back to list view
     *
     *
     * @param array The form data
     * @param Form The form object
     */
    public function doSaveAndQuit($data, $form)
    {
        Controller::curr()->getResponse()->addHeader("X-Pjax", "Content");
        return $this->saveAndRedirect($data, $form, $this->getBackLink());
    }

    /**
     * Publishes the record and goes to make a new record
     * @param  array $data The form data
     * @param  Form $form The Form object
     * @return HTTPResponse
     */
    public function doPublishAndAdd($data, $form)
    {
        return $this->publish($data, $form, $this->owner, $this->owner->Link('addnew'));
    }

    /**
     * Publishes the record and closes the detail form
     * @param  array $data The form data
     * @param  Form $form The Form object
     * @return HTTPResponse
     */
    public function doPublishAndClose($data, $form)
    {
        Controller::curr()->getResponse()->addHeader("X-Pjax", "Content");
        return $this->publish($data, $form, $this->owner, $this->getBackLink());
    }

    /**
     * Goes back to list view
     *
     * @param array The form data
     * @param Form The form object
     */
    public function cancel()
    {
        Controller::curr()->getResponse()->addHeader("X-Pjax", "Content");
        return Controller::curr()->redirect($this->getBackLink());
    }

    /**
     * Saves the record and goes to the next one
     * @param  arary $data The form data
     * @param  Form $form The Form object
     * @return HTTPResponse
     */
    public function doSaveAndNext($data, $form)
    {
        Controller::curr()->getResponse()->addHeader("X-Pjax", "Content");
        $link = $this->getEditLink($this->getNextRecordID());

        return $this->saveAndRedirect($data, $form, $link);
    }

    /**
     * Saves the record and goes to the previous one
     * @param  arary $data The form data
     * @param  Form $form The Form object
     * @return HTTPResponse
     */
    public function doSaveAndPrev($data, $form)
    {
        Controller::curr()->getResponse()->addHeader("X-Pjax", "Content");
        $link = $this->getEditLink($this->getPreviousRecordID());

        return $this->saveAndRedirect($data, $form, $link);
    }

    /**
     * Gets the edit link for a record
     * @param  int $id The ID of the record in the GridField
     * @return string
     */
    public function getEditLink($id)
    {
        return Controller::join_links($this->owner->gridField->Link(), "item", $id);
    }

    /**
     * Creates a new record. If you're already creating a new record,
     * this forces the URL to change. Hacky UI workaround.
     *
     * @param  arary $data The form data
     * @param  Form $form The Form object
     * @return HTTPResponse
     */
    public function doNew($data, $form)
    {
        return Controller::curr()->redirect($this->owner->Link('addnew'));
    }

    /**
     * Allows us to have our own configurable save button
     * @param  arary $data The form data
     * @param  Form $form The Form object
     * @return HTTPResponse
     */
    public function save($data, $form)
    {
        $origStage = Versioned::get_stage();
        Versioned::set_stage('Stage');
        $action = $this->owner->doSave($data, $form);
        Versioned::set_stage($origStage);

        return $action;
    }

    /**
     * @param  array       $data
     * @param  Form        $form
     * @param  HTTPRequest $request
     * @param  string      $redirectURL
     * @return HTMLText|HTTPResponse|ViewableData_Customised
     */
    public function publish($data, $form, $request = null, $redirectURL = null)
    {
        $new_record = $this->owner->record->ID == 0;
        $controller = Controller::curr();
        $list = $this->owner->gridField->getList();

        if ($list instanceof ManyManyList) {
            // Data is escaped in ManyManyList->add()
            $extraData = (isset($data['ManyMany'])) ? $data['ManyMany'] : null;
        } else {
            $extraData = null;
        }

        if (isset($data['ClassName']) && $data['ClassName'] != $this->owner->record->ClassName) {
            $newClassName = $data['ClassName'];
            // The records originally saved attribute was overwritten by $form->saveInto($record) before.
            // This is necessary for newClassInstance() to work as expected, and trigger change detection
            // on the ClassName attribute
            $this->owner->record->setClassName($this->owner->record->ClassName);
            // Replace $record with a new instance
            $this->owner->record = $this->owner->record->newClassInstance($newClassName);
        }

        if (!$this->owner->record->canEdit()) {
            return $controller->httpError(403);
        }

        try {
            $this->save($data, $form);
            $list->add($this->owner->record, $extraData);
            $this->owner->record->invokeWithExtensions('onBeforePublish', $this->owner->record);
            $this->owner->record->publish('Stage', 'Live');
            $this->owner->record->invokeWithExtensions('onAfterPublish', $this->owner->record);
        } catch (ValidationException $e) {
            $form->sessionMessage($e->getResult()->message(), 'bad');
            $responseNegotiator = new PjaxResponseNegotiator(array(
                'CurrentForm' => function () use (&$form) {
                    return $form->forTemplate();
                },
                'default'     => function () use (&$controller) {
                    return $controller->redirectBack();
                }
            ));
            if ($controller->getRequest()->isAjax()) {
                $controller->getRequest()->addHeader('X-Pjax', 'CurrentForm');
            }

            return $responseNegotiator->respond($controller->getRequest());
        }

        // TODO Save this item into the given relationship

        if ($redirectURL) {
            return $controller->redirect($redirectURL);
        }

        $title = '"' . Convert::raw2xml($this->owner->record->Title) . '"';
        $message = sprintf(
            'Published %s %s',
            $this->owner->record->i18n_singular_name(),
            $title
        );

        $form->sessionMessage($message, 'good');

        if ($new_record) {
            return Controller::curr()->redirect($this->owner->Link());
        } elseif ($this->owner->gridField->getList()->byId($this->owner->record->ID)) {
            // Return new view, as we can't do a "virtual redirect" via the CMS Ajax
            // to the same URL (it assumes that its content is already current, and doesn't reload)
            return $this->owner->edit(Controller::curr()->getRequest());
        } else {
            // Changes to the record properties might've excluded the record from
            // a filtered list, so return back to the main view if it can't be found
            $noActionURL = $controller->removeAction($data['url']);
            $controller->getRequest()->addHeader('X-Pjax', 'Content');

            return $controller->redirect($noActionURL, 302);
        }
    }

    /**
     * Unpublishes the record
     *
     * @return HTMLText|ViewableData_Customised
     */
    public function unPublish()
    {
        $origStage = Versioned::get_stage();
        Versioned::set_stage('Live');

        // This way our ID won't be unset
        $clone = clone $this->owner->record;
        $clone->delete();

        Versioned::set_stage($origStage);

        return $this->owner->edit(Controller::curr()->getRequest());
    }

    /**
     * @param  array $data
     * @param  Form  $form
     * @return HTMLText|ViewableData_Customised
     */
    public function rollback($data, $form)
    {
        if (!$this->owner->record->canEdit()) {
            return Controller::curr()->httpError(403);
        }

        $this->owner->record->doRollbackTo('Live');

        $this->owner->record = DataList::create($this->owner->record->class)->byID($this->owner->record->ID);

        $message = _t(
            'CMSMain.ROLLEDBACKPUBv2',
            "Rolled back to published version."
        );

        $form->sessionMessage($message, 'good');

        return $this->owner->edit(Controller::curr()->getRequest());
    }

    /**
     * Gets the top level controller.
     *
     * @return Controller
     * @todo  This had to be directly copied from {@link GridFieldDetailForm_ItemRequest}
     * because it is a protected method and not visible to a decorator!
     */
    protected function getToplevelController()
    {
        $c = $this->owner->getController();
        while ($c && $c instanceof GridFieldDetailForm_ItemRequest) {
            $c = $c->getController();
        }
        return $c;
    }

    /**
     * Gets the back link
     *
     * @return  string
     * @todo  This had to be directly copied from {@link GridFieldDetailForm_ItemRequest}
     * because it is a protected method and not visible to a decorator!
     */
    public function getBackLink()
    {
        // TODO Coupling with CMS
        $backlink = '';
        $toplevelController = $this->getToplevelController();
        if ($toplevelController && $toplevelController instanceof LeftAndMain) {
            if ($toplevelController->hasMethod('Backlink')) {
                $backlink = $toplevelController->Backlink();
            } elseif ($this->owner->getController()->hasMethod('Breadcrumbs')) {
                $parents = $this->owner->getController()->Breadcrumbs(false)->items;
                $backlink = array_pop($parents)->Link;
            }
        }
        if (!$backlink) {
            $backlink = $toplevelController->Link();
        }

        return $backlink;
    }

    /**
     * Oh, the horror! DRY police be advised. This function is a serious offender.
     * Saves the form data and redirects to a given link
     *
     * @param array  $data         The form data
     * @param Form   $form         The form object
     * @param string $redirectLink The redirect link
     * @todo  GridFieldDetailForm_ItemRequest::doSave is too monolithic, making overloading impossible. Most
     *        of this code is a direct copy.
     * */
    protected function saveAndRedirect($data, $form, $redirectLink)
    {
        $new_record = $this->owner->record->ID == 0;
        $controller = Controller::curr();
        $list = $this->owner->gridField->getList();

        if ($list instanceof ManyManyList) {
            // Data is escaped in ManyManyList->add()
            $extraData = (isset($data['ManyMany'])) ? $data['ManyMany'] : null;
        } else {
            $extraData = null;
        }

        if (!$this->owner->record->canEdit()) {
            return $controller->httpError(403);
        }

        try {
            $form->saveInto($this->owner->record);
            $this->owner->record->write();
            $list->add($this->owner->record, $extraData);
        } catch (ValidationException $e) {
            $form->sessionMessage($e->getResult()->message(), 'bad');
            $responseNegotiator = new PjaxResponseNegotiator(array(
                'CurrentForm' => function () use ($form) {
                    return $form->forTemplate();
                },
                'default' => function () use ($controller) {
                    return $controller->redirectBack();
                }
            ));
            if ($controller->getRequest()->isAjax()) {
                $controller->getRequest()->addHeader('X-Pjax', 'CurrentForm');
            }
            return $responseNegotiator->respond($controller->getRequest());
        }

        return Controller::curr()->redirect($redirectLink);
    }

    /**
     * Gets the ID of the previous record in the list.
     * WARNING: This does not respect the mutated state of the list (e.g. sorting or filtering).
     * Currently the GridField API does not expose this in the detail form view.
     *
     * @todo  This method is very inefficient.
     * @return int
     */
    public function getPreviousRecordID()
    {
        $map = $this->owner->gridField->getManipulatedList()->limit(PHP_INT_MAX, 0)->column('ID');
        $offset = array_search($this->owner->record->ID, $map);
        return isset($map[$offset-1]) ? $map[$offset-1] : false;
    }

    /**
     * Gets the ID of the next record in the list.
     * WARNING: This does not respect the mutated state of the list (e.g. sorting or filtering).
     * Currently the GridField API does not expose this in the detail form view.
     *
     * @todo  This method is very inefficient.
     * @return int
     */
    public function getNextRecordID()
    {
        $map = $this->owner->gridField->getManipulatedList()->limit(PHP_INT_MAX, 0)->column('ID');
        // If there are a million results and they were paginated, this is going to be slow now
        // TODO: Search in the paginated list only somehow (grab the limit + offset and search from there?)
        $offset = array_search($this->owner->record->ID, $map);
        return isset($map[$offset+1]) ? $map[$offset+1] : false;
    }

    /**
     * Determines if the current record is published
     * @return boolean
     */
    public function recordIsPublished()
    {
        if (!$this->owner->record->checkVersioned()) {
            return false;
        }

        if (!$this->owner->record->isInDB()) {
            return false;
        }

        $baseClass = DataObject::getSchema()->baseDataClass($this->owner->record);
        $stageTable = DataObject::getSchema()->tableName($baseClass) . '_Live';

        return (bool) DB::query("SELECT \"ID\" FROM \"{$stageTable}\" WHERE \"ID\" = {$this->owner->record->ID}")
            ->value();
    }

    /**
     * Determines if the current record is deleted from stage
     * @return boolean
     */
    public function recordIsDeletedFromStage()
    {
        // for SiteTree records
        if ($this->owner->hasMethod('getIsDeletedFromStage')) {
            return $this->owner->IsDeletedFromStage;
        }

        if (!$this->owner->record->checkVersioned()) {
            return false;
        }

        if (!$this->owner->record->isInDB()) {
            return true;
        }

        $class = $this->owner->record->class;

        $stageVersion = Versioned::get_versionnumber_by_stage($class, 'Stage', $this->owner->record->ID);

        // Return true for both completely deleted pages and for pages just deleted from stage
        return !($stageVersion);
    }
}


<?php

namespace UncleCheese\BetterButtons\FormFields;

use Exception;
use SilverStripe\Forms\CompositeField;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest;
use SilverStripe\View\Requirements;
use UncleCheese\BetterButtons\Actions\BetterButtonAction;
use UncleCheese\BetterButtons\Buttons\BetterButton;
use UncleCheese\BetterButtons\Extensions\BetterButtonGroupable;
use UncleCheese\BetterButtons\Interfaces\BetterButtonInterface;

/**
 * Defines the button that holds several form actions and exposes them on click
 *
 * @author  Uncle Cheese <unclecheese@leftandmain.com>
 * @package  silverstripe-gridfield-betterbuttons
 */
class DropdownFormAction extends CompositeField implements BetterButtonInterface
{
    private static $extensions = array(
        BetterButtonGroupable::class
    );

    /**
     * To ensure the buttons get unique ids, keep track of the instances
     * @var integer
     */
    protected static $instance_count = 0;

    /**
     * A unique identifier assigned through $instance_count
     * @var string
     */
    protected $identifier;

    /**
     * Builds the button
     * @param string $title    The text for the button
     * @param array  $children Child buttons (FormActions)
     */
    public function __construct($title = null, $children = array ())
    {
        $this->Title = $title;
        foreach ($children as $c) {
            if ($c instanceof FormAction) {
                $c->setUseButtonTag(true);
            }
        }
        parent::__construct($children);
        self::$instance_count++;
        $this->identifier = self::$instance_count;
    }

    /**
     * Renders the button, includes the JS and CSS
     * @param array $properties
     */
    public function Field($properties = array ())
    {
        Requirements::css(BETTER_BUTTONS_DIR . '/css/dropdown_form_action.css');
        Requirements::javascript(BETTER_BUTTONS_DIR . '/javascript/dropdown_form_action.js');
        $this->setAttribute('data-form-action-dropdown', '#' . $this->DropdownID());

        return parent::Field();
    }

    /**
     * A unique id for the dropdown button
     *
     * @return  string
     */
    public function DropdownID()
    {
        return 'form-action-dropdown-' . $this->identifier;
    }

    /**
     * Determines if the button should displsy
     * @return boolean
     */
    public function shouldDisplay()
    {
        foreach ($this->children as $child) {
            if ($child->shouldDisplay()) {
                return true;
            }
        }

        return false;
    }

    /**
     * Binds to the GridField request, and transforms the buttons
     * @param Form $form
     * @param GridFieldDetailForm_ItemRequest $request
     * @return $this
     * @throws Exception if instances of BetterButton are not passed
     */
    public function bindGridField(Form $form, GridFieldDetailForm_ItemRequest $request)
    {
        $this->setForm($form);
        $this->gridFieldRequest = $request;

        foreach ($this->children as $child) {
            if (!$child instanceof BetterButton && !$child instanceof BetterButtonAction) {
                throw new Exception("DropdownFormAction must be passed instances of BetterButton");
            }

            $child->bindGridField($form, $request);
            $child->setIsGrouped(true);

            if ($child instanceof FormAction) {
                $child->setUseButtonTag(true);
            }
        }

        return $this;
    }
}


<?php

namespace UncleCheese\BetterButtons\Actions;

use SilverStripe\Control\Controller;
use UncleCheese\BetterButtons\Actions\BetterButtonAction;

/**
 * Defines a set of buttons that offers prev/next navigation from within a
 * GridField detail form
 *
 * @author  Uncle Cheese <unclecheese@leftandmain.com>
 * @package  silverstripe-gridfield-betterbuttons
 */
class BetterButtonPrevNextAction extends BetterButtonAction
{
    /**
     * Gets the HTML for the button
     * @return string
     */
    public function getButtonHTML()
    {
        $html = '
';

        // Prev/next links. Todo: This doesn't scale well.

        // Check if the gridfield as been filtered
        $params = array(
            'q' => (array)$this->gridFieldRequest->getRequest()->getVar('q')
        );

        $searchVars = (bool)$params ? '?' . http_build_query($params) : '';

        $previousRecordID = $this->gridFieldRequest->getPreviousRecordID();
        $cssClass = $previousRecordID ? "cms-panel-link" : "disabled";
        $prevLink = $previousRecordID ? Controller::join_links($this->gridFieldRequest->gridField->Link(), "item", $previousRecordID . $searchVars) : "javascript:void(0);";
        $linkTitle = $previousRecordID ? _t('GridFieldBetterButtons.PREVIOUSRECORD', 'Go to the previous record') : "";
        $linkText = $previousRecordID ? _t('GridFieldBetterButtons.PREVIOUS', 'Previous') : "";

        $html .= sprintf(
            " %s",
            $cssClass,
            $prevLink,
            $linkTitle,
            $linkText
        );

        $nextRecordID = $this->gridFieldRequest->getNextRecordID();
        $cssClass = $nextRecordID ? "cms-panel-link" : "disabled";
        $nextLink = $nextRecordID ? Controller::join_links($this->gridFieldRequest->gridField->Link(), "item", $nextRecordID . $searchVars) : "javascript:void(0);";

        $linkTitle = $nextRecordID ? _t('GridFieldBetterButtons.NEXTRECORD', 'Go to the next record') : "";
        $linkText = $nextRecordID ? _t('GridFieldBetterButtons.NEXT', 'Next') : "";

        $html .= sprintf(
            "%s ",
            $cssClass,
            $nextLink,
            $linkTitle,
            $linkText
        );

        $html .= '
';

        return $html;
    }
}


<?php  namespace Grambas\FootballData;

use GuzzleHttp\Client;

class FootballData
{
	protected $client;

	public function __construct(Client $client )
	{
		$this->client = $client;
	}

	public function run($uri, $type = 'GET')
	{
		return json_decode( $this->client->request($type, $uri)->getBody() );
	}

	##COMPETITION/LEAGUE

	/**
	 * List all available competitions.
	 *
	 * @param array $filter
	 * @return Collection
	 */
	public function getLeagues(array $filter = ['areas' => ''])
	{
		$leagues = $this->run("v2/competitions"."?".http_build_query($filter) );
		return collect($leagues->competitions);
	}

	/**
	 * List one particular competition.
	 *
	 * @param integer $leagueID
	 * @param array $filter
	 * @return Collection
	 */
	public function getLeague(int $leagueID, array $filter = ['areas' => ''])
	{
		$league = $this->run("v2/competitions/{$leagueID}"."?".http_build_query($filter));
		return collect($league);
	}

	/**
	 * List all teams for a particular competition.
	 *
	 * @param integer $leagueID
	 * @param array $filter
	 * @return Collection
	 */
	public function  getLeagueTeams(int $leagueID, array $filter = ['stage' => ''])
	{
		$leagueTeams = $this->run("v2/competitions/{$leagueID}/teams"."?".http_build_query($filter));
		return collect($leagueTeams->teams);
	}

	/**
	 * Show Standings for a particular competition
	 *
	 * @param integer $leagueID
	 * @return Collection
	 */
	public function getLeagueStandings(int $leagueID)
	{
		$leagueStandings = $this->run("v2/competitions/{$leagueID}/standings");
		return collect($leagueStandings->standings);
	}

	/**
	 * List all matches for a particular competition.
	 *
	 * @param integer $leagueID
	 * @param array $filter
	 * @return Collection
	 */
	public function getLeagueMatches(int $leagueID, array $filter = [ 'dateFrom' => '', 'dateTo' => '', 'stage' => '', 'status' => '', 'matchday' => '', 'group' => '' ])
	{
		$leagueMatches = $this->run("v2/competitions/{$leagueID}/matches"."?".http_build_query($filter));
		return collect($leagueMatches->matches);
	}

	##FIXTURES/MATCHES

	/**
	 * List matches across (a set of) competitions.
	 *
	 * @param array $filter
	 * @return Collection
	 */
	public function getMatches(array $filter = [ 'competitions' => '', 'dateFrom' => '', 'dateTo' => '', 'status' => '' ])
	{
		$matches = $this->run("v2/matches"."?".http_build_query($filter));
		return collect($matches->matches);
	}

	/**
	 * Show one particular match.
	 *
	 * @param integer $matchID
	 * @return Collection
	 */
	public function getMatche(int $matchID)
	{
		$matche = $this->run("v2/matches/{$matchID}");
		return collect($matche);
	}

	##TEAM

	/**
	 * Show one particular team.
	 *
	 * @param integer $teamID
	 * @return Collection
	 */
	public function getTeam(int $teamID)
	{
		$team = $this->run("v2/teams/{$teamID}");
		return collect($team);
	}

	/**
	 * Show all matches for a particular team.
	 *
	 * @param integer $teamID
	 * @param array $filter
	 * @return Collection
	 */
	public function getMatchesForTeam(int $teamID, array $filter = [ 'dateFrom' => '', 'dateTo' => '', 'status' => '', 'venue' => '' ])
	{
		$matches = $this->run("v2/teams/{$teamID}/matches"."?".http_build_query($filter));
		return collect($matches->matches);
	}

	##AREAS

	/**
	 * List all available areas.
	 *
	 * @return Collection
	 */
	public function getAreas()
	{
		$areas = $this->run("v2/areas");
		return collect($areas->areas);
	}

	/**
	 * List one particular area.
	 *
	 * @param integer $areaID
	 * @return Collection
	 */
	public function getArea(int $areaID)
	{
		$area = $this->run("v2/areas/{$areaID}");
		return collect($area);
	}

}


<?php

namespace ResqueBundle\Resque\Controller;

use ResqueBundle\Resque\Resque;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * Class DefaultController
 * @package ResqueBundle\Resque\Controller
 */
class DefaultController extends Controller
{
    /**
     * @return Response
     */
    public function indexAction()
    {
        $this->getResque()->pruneDeadWorkers();

        return $this->render(
            'ResqueBundle:Default:index.html.twig',
            [
                'resque' => $this->getResque(),
            ]
        );
    }

    /**
     * @return Resque
     */
    protected function getResque()
    {
        return $this->get('ResqueBundle\Resque\Resque');
    }

    /**
     * @param $queue
     * @param Request $request
     * @return Response
     */
    public function showQueueAction($queue, Request $request)
    {
        list($start, $count, $showingAll) = $this->getShowParameters($request);

        $queue = $this->getResque()->getQueue($queue);
        $jobs = $queue->getJobs($start, $count);

        if (!$showingAll) {
            $jobs = array_reverse($jobs);
        }

        return $this->render(
            'ResqueBundle:Default:queue_show.html.twig',
            [
                'queue'      => $queue,
                'jobs'       => $jobs,
                'showingAll' => $showingAll
            ]
        );
    }

    public function removeQueueAction($queue, Request $request)
    {
        $queue = $this->getResque()->getQueue($queue);
        $count = $queue->clear();
        $queue->remove();

        $this->addFlash('info', 'Remove ' . $queue->getName() . ' queue and ' . $count . ' jobs.');

        return $this->redirectToRoute('ResqueBundle_homepage');
    }

    /**
     * Decide which parts of a job queue to show
     *
     * @param Request $request
     *
     * @return array
     */
    private function getShowParameters(Request $request)
    {
        $showingAll = FALSE;
        $start = -100;
        $count = -1;

        if ($request->query->has('all')) {
            $start = 0;
            $count = -1;
            $showingAll = TRUE;
        }

        return [$start, $count, $showingAll];
    }

    /**
     * @param Request $request
     * @return Response
     */
    public function listFailedAction(Request $request)
    {
        list($start, $count, $showingAll) = $this->getShowParameters($request);

        $jobs = $this->getResque()->getFailedJobs($start, $count);

        if (!$showingAll) {
            $jobs = array_reverse($jobs);
        }

        return $this->render(
            'ResqueBundle:Default:failed_list.html.twig',
            [
                'jobs'       => $jobs,
                'showingAll' => $showingAll,
            ]
        );
    }

    /**
     * @return Response
     */
    public function workersAction()
    {
        return $this->render(
            'ResqueBundle:Default:workers.html.twig',
            [
                'resque' => $this->getResque(),
            ]
        );
    }

    /**
     * @return Response
     */
    public function listScheduledAction()
    {
        return $this->render(
            'ResqueBundle:Default:scheduled_list.html.twig',
            [
                'timestamps' => $this->getResque()->getDelayedJobTimestamps()
            ]
        );
    }

    /**
     * @param $timestamp
     * @return Response
     */
    public function showTimestampAction($timestamp)
    {
        $jobs = [];

        // we don't want to enable the twig debug extension for this...
        foreach ($this->getResque()->getJobsForTimestamp($timestamp) as $job) {
            $jobs[] = print_r($job, TRUE);
        }

        return $this->render(
            'ResqueBundle:Default:scheduled_timestamp.html.twig',
            [
                'timestamp' => $timestamp,
                'jobs'      => $jobs
            ]
        );
    }

    /**
     * @return RedirectResponse
     */
    public function retryFailedAction()
    {
        $count = $this->getResque()->retryFailedJobs();

        $this->addFlash('info', 'Retry '.$count.' failed jobs.');

        return $this->redirectToRoute('ResqueBundle_homepage');
    }

    /**
     * @return RedirectResponse
     */
    public function retryClearFailedAction()
    {
        $count = $this->getResque()->retryFailedJobs(true);

        $this->addFlash('info', 'Retry and clear '.$count.' failed jobs.');

        return $this->redirectToRoute('ResqueBundle_homepage');
    }

    /**
     * @return RedirectResponse
     */
    public function clearFailedAction()
    {
        $count = $this->getResque()->clearFailedJobs();

        $this->addFlash('info', 'Clear '.$count.' failed jobs.');

        return $this->redirectToRoute('ResqueBundle_homepage');
    }
}


<?php

namespace ResqueBundle\Resque\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;

/**
 * This is the class that loads and manages your bundle configuration
 *
 * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html}
 */
class ResqueExtension extends Extension
{
    /**
     * {@inheritDoc}
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);

        $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
        $loader->load('services.xml');

        $container->setParameter('resque.vendor_dir', $config['vendor_dir']);
        $container->setParameter('resque.app_include', $config['app_include']);
        $container->setParameter('resque.class', $config['class']);
        $container->setParameter('resque.redis.host', $config['redis']['host']);
        $container->setParameter('resque.redis.port', $config['redis']['port']);
        $container->setParameter('resque.redis.database', $config['redis']['database']);

        if (!empty($config['prefix'])) {
            $container->setParameter('resque.prefix', $config['prefix']);
            $container->getDefinition('ResqueBundle\Resque\Resque')->addMethodCall('setPrefix', [$config['prefix']]);
        }

        if (!empty($config['worker']['root_dir'])) {
            $container->setParameter('resque.worker.root_dir', $config['worker']['root_dir']);
        }

        if (!empty($config['auto_retry'])) {
            if (isset($config['auto_retry'][0])) {
                $container->getDefinition('ResqueBundle\Resque\Resque')->addMethodCall('setGlobalRetryStrategy', [$config['auto_retry'][0]]);
            } else {
                if (isset($config['auto_retry']['default'])) {
                    $container->getDefinition('ResqueBundle\Resque\Resque')->addMethodCall('setGlobalRetryStrategy', [$config['auto_retry']['default']]);
                    unset($config['auto_retry']['default']);
                }
                $container->getDefinition('ResqueBundle\Resque\Resque')->addMethodCall('setJobRetryStrategy', [$config['auto_retry']]);
            }
        }
    }
}


<?php

namespace ResqueBundle\Resque\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

/**
 * This is the class that validates and merges configuration from your app/config files
 *
 * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class}
 */
class Configuration implements ConfigurationInterface
{
    /**
     * {@inheritDoc}
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('resque');

        $rootNode
            ->addDefaultsIfNotSet()
            ->children()
                ->scalarNode('vendor_dir')
                    ->defaultValue('%kernel.root_dir%/../vendor')
                    ->cannotBeEmpty()
                    ->info('Set the vendor dir')
                ->end()
                ->scalarNode('app_include')
                    ->defaultValue('%kernel.root_dir%/../var/bootstrap.php.cache')
                    ->cannotBeEmpty()
                    ->info('Set the APP_INCLUDE for php-resque')
                ->end()
                ->scalarNode('prefix')
                    ->defaultNull()
                    ->end()
                ->scalarNode('class')
                    ->defaultValue('ResqueBundle\Resque\Resque')
                    ->cannotBeEmpty()
                    ->info('Set the resque class dir')
                ->end()
                ->arrayNode('auto_retry')
                    ->beforeNormalization()
                        ->ifArray()
                        ->then(function($var) {
                            if (array_key_exists(0, $var)) {
                                return [$var];
                            }
                            return $var;
                        })
                    ->end()
                    ->prototype('array')
                        ->prototype('scalar')->end()
                    ->end()
                    ->info('Set auto retry strategy')
                ->end()
                ->arrayNode('redis')
                    ->info('Redis configuration')
                    ->addDefaultsIfNotSet()
                    ->children()
                        ->scalarNode('host')
                            ->defaultValue('localhost')
                            ->cannotBeEmpty()
                            ->info('The redis hostname')
                        ->end()
                        ->scalarNode('port')
                            ->defaultValue(6379)
                            ->cannotBeEmpty()
                            ->info('The redis port')
                        ->end()
                        ->scalarNode('database')
                            ->defaultValue(0)
                            ->info('The redis database')
                        ->end()
                    ->end()
                ->end()
                ->arrayNode('worker')
                    ->info('Worker Server configuration')
                    ->addDefaultsIfNotSet()
                    ->children()
                        ->scalarNode('root_dir')
                            ->defaultValue('%kernel.root_dir%')
                            ->cannotBeEmpty()
                            ->info('The root dir of worker registered app')
                        ->end()
                    ->end()
                ->end()
            ->end()
        ;

        return $treeBuilder;
    }
}


<?php

namespace ZfcTwig\View;

use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class TwigRendererFactory implements FactoryInterface
{
    /**
     * Create service
     *
     * @param ServiceLocatorInterface $serviceLocator
     * @return TwigRenderer
     */
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        /** @var \ZfcTwig\moduleOptions $options */
        $options = $serviceLocator->get('ZfcTwig\ModuleOptions');

        $renderer = new TwigRenderer(
            $serviceLocator->get('Zend\View\View'),
            $serviceLocator->get('Twig_Loader_Chain'),
            $serviceLocator->get('Twig_Environment'),
            $serviceLocator->get('ZfcTwig\View\TwigResolver')
        );

        $renderer->setCanRenderTrees($options->getDisableZfmodel());
        $renderer->setHelperPluginManager($serviceLocator->get('ZfcTwigViewHelperManager'));

        return $renderer;
    }
}


<?php

namespace ZfcTwig\View;

use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;
use Zend\View\ViewEvent;

class TwigStrategy implements ListenerAggregateInterface
{
    /**
     * @var \Zend\Stdlib\CallbackHandler[]
     */
    protected $listeners = array();

    /**
     * @var TwigRenderer
     */
    protected $renderer;

    /**
     * @param TwigRenderer $renderer
     */
    public function __construct(TwigRenderer $renderer)
    {
        $this->renderer = $renderer;
    }

    /**
     * Attach one or more listeners
     *
     * Implementors may add an optional $priority argument; the EventManager
     * implementation will pass this to the aggregate.
     *
     * @param EventManagerInterface $events
     * @param int $priority
     */
    public function attach(EventManagerInterface $events, $priority = 100)
    {
        $this->listeners[] = $events->attach(ViewEvent::EVENT_RENDERER, array($this, 'selectRenderer'), $priority);
        $this->listeners[] = $events->attach(ViewEvent::EVENT_RESPONSE, array($this, 'injectResponse'), $priority);
    }

    /**
     * Detach all previously attached listeners
     *
     * @param EventManagerInterface $events
     */
    public function detach(EventManagerInterface $events)
    {
        foreach ($this->listeners as $index => $listener) {
            $events->detach($listener);
            unset($this->listeners[$index]);
        }
    }

    /**
     * Determine if the renderer can load the requested template.
     *
     * @param ViewEvent $e
     * @return bool|TwigRenderer
     */
    public function selectRenderer(ViewEvent $e)
    {
        if ($this->renderer->canRender($e->getModel()->getTemplate())) {
            return $this->renderer;
        }
        return false;
    }

    /**
     * Inject the response from the renderer.
     *
     * @param \Zend\View\ViewEvent $e
     */
    public function injectResponse(ViewEvent $e)
    {
        $renderer = $e->getRenderer();
        if ($renderer !== $this->renderer) {
            return;
        }
        $result   = $e->getResult();
        $response = $e->getResponse();

        $response->setContent($result);
    }
}


<?php

namespace ZfcTwig\Twig;

use InvalidArgumentException;
use Twig_Loader_Chain;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class ChainLoaderFactory implements FactoryInterface
{
    /**
     * Create service
     *
     * @param ServiceLocatorInterface $serviceLocator
     * @throws \InvalidArgumentException
     * @return Twig_Loader_Chain
     */
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        /** @var \ZfcTwig\moduleOptions $options */
        $options = $serviceLocator->get('ZfcTwig\ModuleOptions');

        // Setup loader
        $chain = new Twig_Loader_Chain();

        foreach ($options->getLoaderChain() as $loader) {
            if (!is_string($loader) || !$serviceLocator->has($loader)) {
                throw new InvalidArgumentException('Loaders should be a service manager alias.');
            }
            $chain->addLoader($serviceLocator->get($loader));
        }

        return $chain;
    }
}


<?php

namespace ZfcTwig\Twig;

use Twig_Error_Loader;
use Twig_Loader_Filesystem;

class StackLoader extends Twig_Loader_Filesystem
{
    /**
     * Default suffix to use
     *
     * Appends this suffix if the template requested does not use it.
     *
     * @var string
     */
    protected $defaultSuffix = 'twig';

    /**
     * Set default file suffix
     *
     * @param  string $defaultSuffix
     * @return StackLoader
     */
    public function setDefaultSuffix($defaultSuffix)
    {
        $this->defaultSuffix = (string) $defaultSuffix;
        $this->defaultSuffix = ltrim($this->defaultSuffix, '.');
        return $this;
    }

    /**
     * Get default file suffix
     *
     * @return string
     */
    public function getDefaultSuffix()
    {
        return $this->defaultSuffix;
    }

    protected function findTemplate($name)
    {
        $name = (string) $name;

        // normalize name
        $name = preg_replace('#/{2,}#', '/', strtr($name, '\\', '/'));

        if (isset($this->cache[$name])) {
            return $this->cache[$name];
        }

        // Ensure we have the expected file extension
        $defaultSuffix = $this->getDefaultSuffix();
        if (pathinfo($name, PATHINFO_EXTENSION) != $defaultSuffix) {;
            $name .= '.' . $defaultSuffix;
        }

        $this->validateName($name);

        $namespace = '__main__';
        if (isset($name[0]) && '@' == $name[0]) {
            if (false === $pos = strpos($name, '/')) {
                throw new Twig_Error_Loader(sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name));
            }

            $namespace = substr($name, 1, $pos - 1);

            $name = substr($name, $pos + 1);
        }

        if (!isset($this->paths[$namespace])) {
            throw new Twig_Error_Loader(sprintf('There are no registered paths for namespace "%s".', $namespace));
        }

        foreach ($this->paths[$namespace] as $path) {
            if (is_file($path.'/'.$name)) {
                return $this->cache[$name] = $path.'/'.$name;
            }
        }

        throw new Twig_Error_Loader(sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace])));
    }
}


<?php

namespace ZfcTwig\View;

use Twig_Environment;
use Twig_Error_Loader;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;
use Zend\Http\Response;
use Zend\Mvc\MvcEvent;
use Zend\View\Model\ViewModel;

/**
 * @category   Zend
 * @package    Zend_Mvc
 * @subpackage View
 */
class RenderingStrategy implements ListenerAggregateInterface
{
    /**
     * @var \Zend\Stdlib\CallbackHandler[]
     */
    protected $listeners = array();

    /**
     * @var Twig_Environment
     */
    protected $environment;

    /**
     * Attach one or more listeners
     *
     * Implementors may add an optional $priority argument; the EventManager
     * implementation will pass this to the aggregate.
     *
     * @param EventManagerInterface $events
     */
    public function attach(EventManagerInterface $events)
    {
        $this->listeners[] = $events->attach(MvcEvent::EVENT_RENDER, array($this, 'render'), -999);
    }

    /**
     * Detach all previously attached listeners
     *
     * @param EventManagerInterface $events
     */
    public function detach(EventManagerInterface $events)
    {
        foreach ($this->listeners as $index => $listener) {
            if ($events->detach($listener)) {
                unset($this->listeners[$index]);
            }
        }
    }

    /**
     * @param Twig_Environment $environment
     * @return RenderingStrategy
     */
    public function setEnvironment(Twig_Environment $environment)
    {
        $this->environment = $environment;
        return $this;
    }

    /**
     * @return Twig_Environment
     */
    public function getEnvironment()
    {
        return $this->environment;
    }

    /**
     * Render the view
     *
     * @param  MvcEvent $e
     * @return null|Response
     */
    public function render(MvcEvent $e)
    {
        $result = $e->getResult();
        if ($result instanceof Response) {
            return $result;
        }

        // Martial arguments
        $response  = $e->getResponse();
        $viewModel = $e->getViewModel();
        if (!$viewModel instanceof ViewModel) {
            return null;
        }

        try {
            $result = $this->getEnvironment()->render(
                $viewModel->getTemplate() . $this->getSuffix(),
                (array) $viewModel->getVariables()
            );
        } catch (Twig_Error_Loader $e) {
            return null;
        }

        $response->setContent($result);
        $e->setResult($response);

        return $response;
    }
}


<?php

namespace ZfcTwig\Twig;

use Twig_Error_Loader;
use Twig_ExistsLoaderInterface;
use Twig_LoaderInterface;

class MapLoader implements Twig_ExistsLoaderInterface, Twig_LoaderInterface
{
    /**
     * Array of templates to filenames.
     * @var array
     */
    protected $map = array();

    /**
     * Add to the map.
     *
     * @param string $name
     * @param string $path
     * @throws \Twig_Error_Loader
     * @return MapLoader
     */
    public function add($name, $path)
    {
        if ($this->exists($name)) {
            throw new Twig_Error_Loader(sprintf(
                'Name "%s" already exists in map',
                $name
            ));
        }
        $this->map[$name] = $path;
        return $this;
    }

    /**
     * {@inheritDoc}
     */
    public function exists($name)
    {
        return array_key_exists($name, $this->map);
    }

    /**
     * {@inheritDoc}
     */
    public function getSource($name)
    {
        if (!$this->exists($name)) {
            throw new Twig_Error_Loader(sprintf(
                'Unable to find template "%s" from template map',
                $name
            ));
        }
        if(!file_exists($this->map[$name])) {
            throw new Twig_Error_Loader(sprintf(
                'Unable to open file "%s" from template map',
                $this->map[$name]
            ));
        }
        return file_get_contents($this->map[$name]);
    }

    /**
     * {@inheritDoc}
     */
    public function getCacheKey($name)
    {
        return $name;
    }

    /**
     * {@inheritDoc}
     */
    public function isFresh($name, $time)
    {
        return filemtime($this->map[$name]) <= $time;
    }
}


<?php

namespace ZfcTwig\View;

use Twig_Environment;
use Twig_Loader_Chain;
use Zend\View\Exception;
use Zend\View\HelperPluginManager;
use Zend\View\Model\ModelInterface;
use Zend\View\Renderer\RendererInterface;
use Zend\View\Renderer\TreeRendererInterface;
use Zend\View\Resolver\ResolverInterface;
use Zend\View\View;

class TwigRenderer implements RendererInterface, TreeRendererInterface
{
    /**
     * @var bool
     */
    protected $canRenderTrees = true;

    /**
     * @var Twig_Environment
     */
    protected $environment;

    /**
     * @var HelperPluginManager
     */
    protected $helperPluginManager;

    /**
     * @var Twig_Loader_Chain
     */
    protected $loader;

    /**
     * @var TwigResolver
     */
    protected $resolver;

    /**
     * @var \Zend\View\View
     */
    protected $view;

    /**
     * @var array Cache for the plugin call
     */
    private $__pluginCache = array();

    /**
     * @param View $view
     * @param \Twig_Loader_Chain $loader
     * @param Twig_Environment $environment
     * @param TwigResolver $resolver
     */
    public function __construct(
        View $view,
        Twig_Loader_Chain $loader,
        Twig_Environment $environment,
        TwigResolver $resolver
    ) {
        $this->environment = $environment;
        $this->loader      = $loader;
        $this->resolver    = $resolver;
        $this->view        = $view;
    }

    /**
     * Overloading: proxy to helpers
     *
     * Proxies to the attached plugin manager to retrieve, return, and potentially
     * execute helpers.
     *
     * * If the helper does not define __invoke, it will be returned
     * * If the helper does define __invoke, it will be called as a functor
     *
     * @param  string $method
     * @param  array $argv
     * @return mixed
     */
    public function __call($method, $argv)
    {
        if (!isset($this->__pluginCache[$method])) {
            $this->__pluginCache[$method] = $this->plugin($method);
        }
        if (is_callable($this->__pluginCache[$method])) {
            return call_user_func_array($this->__pluginCache[$method], $argv);
        }
        return $this->__pluginCache[$method];
    }

    /**
     * @param boolean $canRenderTrees
     * @return TwigRenderer
     */
    public function setCanRenderTrees($canRenderTrees)
    {
        $this->canRenderTrees = $canRenderTrees;
        return $this;
    }

    /**
     * @return boolean
     */
    public function canRenderTrees()
    {
        return $this->canRenderTrees;
    }

    /**
     * Get plugin instance, proxy to HelperPluginManager::get
     *
     * @param  string     $name Name of plugin to return
     * @param  null|array $options Options to pass to plugin constructor (if not already instantiated)
     * @return \Zend\View\Helper\AbstractHelper
     */
    public function plugin($name, array $options = null)
    {
        return $this->getHelperPluginManager()
                    ->setRenderer($this)
                    ->get($name, $options);
    }

    /**
     * Can the template be rendered?
     *
     * @param string $name
     * @return bool
     * @see \ZfcTwig\Twig\Environment::canLoadTemplate()
     */
    public function canRender($name)
    {
        return $this->loader->exists($name);
    }

    /**
     * Return the template engine object, if any
     *
     * If using a third-party template engine, such as Smarty, patTemplate,
     * phplib, etc, return the template engine object. Useful for calling
     * methods on these objects, such as for setting filters, modifiers, etc.
     *
     * @return Twig_Environment
     */
    public function getEngine()
    {
        return $this->environment;
    }

    /**
     * Set the resolver used to map a template name to a resource the renderer may consume.
     *
     * @param  ResolverInterface $resolver
     * @return TwigRenderer
     */
    public function setResolver(ResolverInterface $resolver)
    {
        $this->resolver = $resolver;
        return $this;
    }

    /**
     * @param HelperPluginManager $helperPluginManager
     * @return TwigRenderer
     */
    public function setHelperPluginManager(HelperPluginManager $helperPluginManager)
    {
        $helperPluginManager->setRenderer($this);
        $this->helperPluginManager = $helperPluginManager;
        return $this;
    }

    /**
     * @return \Zend\View\HelperPluginManager
     */
    public function getHelperPluginManager()
    {
        return $this->helperPluginManager;
    }

    /**
     * Processes a view script and returns the output.
     *
     * @param  string|ModelInterface   $nameOrModel The script/resource process, or a view model
     * @param  null|array|\ArrayAccess $values      Values to use during rendering
     * @return string|null The script output.
     * @throws \Zend\View\Exception\DomainException
     */
    public function render($nameOrModel, $values = array())
    {
        $model = null;

        if ($nameOrModel instanceof ModelInterface) {
            $model       = $nameOrModel;
            $nameOrModel = $model->getTemplate();

            if (empty($nameOrModel)) {
                throw new Exception\DomainException(sprintf(
                    '%s: received View Model argument, but template is empty', __METHOD__
                ));
            }

            $values = (array) $model->getVariables();
        }

        if (!$this->canRender($nameOrModel)) {
            return null;
        }

        if ($model && $this->canRenderTrees() && $model->hasChildren()) {
            if (!isset($values['content'])) {
                $values['content'] = '';
            }
            foreach($model as $child) {
                /** @var \Zend\View\Model\ViewModel $child */
                if ($this->canRender($child->getTemplate())) {
                    $template = $this->resolver->resolve($child->getTemplate(), $this);
                    return $template->render((array) $child->getVariables());
                }
                $child->setOption('has_parent', true);
                $values['content'] .= $this->view->render($child);
            }
        }

        /** @var $template \Twig_Template */
        $template = $this->resolver->resolve($nameOrModel, $this);
        return $template->render((array) $values);
    }
}


<?php

namespace ZfcTwig\Twig;

use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class StackLoaderFactory implements FactoryInterface
{
    /**
     * Create service
     *
     * @param ServiceLocatorInterface $serviceLocator
     * @return mixed
     */
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        /** @var \ZfcTwig\moduleOptions $options */
        $options = $serviceLocator->get('ZfcTwig\ModuleOptions');

        /** @var $templateStack \Zend\View\Resolver\TemplatePathStack */
        $zfTemplateStack = $serviceLocator->get('ViewTemplatePathStack');

        $templateStack = new StackLoader($zfTemplateStack->getPaths()->toArray());
        $templateStack->setDefaultSuffix($options->getSuffix());

        return $templateStack;
    }
}


<?php

namespace ZfcTwig\View;

use Zend\ServiceManager\Config;
use Zend\ServiceManager\ConfigInterface;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\ServiceManager;
use Zend\View\Exception;

class HelperPluginManagerFactory implements FactoryInterface
{

    /**
     * Create service
     *
     * @param ServiceLocatorInterface $serviceLocator
     * @throws \Zend\View\Exception\RuntimeException
     * @return mixed
     */
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        /** @var \ZfcTwig\moduleOptions $options */
        $options        = $serviceLocator->get('ZfcTwig\ModuleOptions');
        $managerOptions = $options->getHelperManager();
        $managerConfigs = isset($managerOptions['configs']) ? $managerOptions['configs'] : array();

        $baseManager = $serviceLocator->get('ViewHelperManager');
        $twigManager = new HelperPluginManager(new Config($managerOptions));
        $twigManager->setServiceLocator($serviceLocator);
        $twigManager->addPeeringServiceManager($baseManager);

        foreach ($managerConfigs as $configClass) {
            if (is_string($configClass) && class_exists($configClass)) {
                $config = new $configClass;

                if (!$config instanceof ConfigInterface) {
                    throw new Exception\RuntimeException(
                        sprintf(
                            'Invalid service manager configuration class provided; received "%s",
                                expected class implementing %s',
                            $configClass,
                            'Zend\ServiceManager\ConfigInterface'
                        )
                    );
                }

                $config->configureServiceManager($twigManager);
            }
        }

        return $twigManager;
    }
}


<?php

namespace ZfcTwig\Twig;

use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class MapLoaderFactory implements FactoryInterface
{
    /**
     * Create service
     *
     * @param ServiceLocatorInterface $serviceLocator
     * @return MapLoader
     */
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        /** @var \ZfcTwig\moduleOptions $options */
        $options = $serviceLocator->get('ZfcTwig\ModuleOptions');

        /** @var \Zend\View\Resolver\TemplateMapResolver */
        $zfTemplateMap = $serviceLocator->get('ViewTemplateMapResolver');

        $templateMap = new MapLoader();
        foreach ($zfTemplateMap as $name => $path) {
            if ($options->getSuffix() == pathinfo($path, PATHINFO_EXTENSION)) {
                $templateMap->add($name, $path);
            }
        }

        return $templateMap;
    }
}


<?php

namespace ChaseConey\LaravelDatadogHelper\Middleware;

use Closure;
use ChaseConey\LaravelDatadogHelper\Datadog;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class LaravelDatadogMiddleware
{

    /**
     * @param $request
     * @param Closure $next
     * @return \Illuminate\Http\RedirectResponse
     */
    public function handle($request, Closure $next)
    {
        $startTime = microtime(true);

        $response = $next($request);

        if (config('datadog-helper.enabled', false)) {
            static::logDuration($request, $response, $startTime);
        }

        return $response;
    }

    /**
     * Logs the duration of a specific request through the application
     *
     * @param Request $request
     * @param Response $response
     * @param double $startTime
     */
    protected static function logDuration(Request $request, Response $response, $startTime)
    {
        $duration = microtime(true) - $startTime;

        $tags = [
            "status_code" => $response->getStatusCode()
        ];

        if (!config('datadog-helper.middleware_disable_url_tag', false)) {
            $tags["url"] = $request->getSchemeAndHttpHost() . $request->getRequestUri();
        }

        Datadog::timing('request_time', $duration, 1, $tags);
    }
}


<?php

namespace ChaseConey\LaravelDatadogHelper;

class LaravelDatadogHelper extends \Datadogstatsd
{

    /**
     * {@inheritdoc}
     */
    public static function send($data, $sampleRate = 1.0, array $tags = null)
    {
        $prefixedData = self::prefixData($data);
        parent::send($prefixedData, $sampleRate, $tags);
    }

    /**
     * Takes normal data array from Datadogstatsd library and adds the Laravel prefix functionality
     *
     * @param $data
     * @return array
     */
    protected static function prefixData($data)
    {
        $prefixedData = array();
        foreach ($data as $stat => $value) {
            $prefixedKey = self::prefix($stat);
            $prefixedData[$prefixedKey] = $value;
        }
        return $prefixedData;
    }

    /**
     * Prefixes a metric name if one is available via config
     *
     * @return string
     */
    protected static function prefix($metric)
    {
        $prefix = config('datadog-helper.prefix');

        if (!$prefix) {
            return $metric;
        }

        return "{$prefix}.{$metric}";
    }
}


<?php

namespace ChaseConey\LaravelDatadogHelper;

use Illuminate\Support\ServiceProvider;

class LaravelDatadogHelperServiceProvider extends ServiceProvider
{

    /**
     * Perform post-registration booting of services.
     *
     * @return void
     */
    public function boot()
    {
        $this->publishes(
            [
                __DIR__ . '/../config/datadog-helper.php' => config_path('datadog-helper.php'),
            ]
        );
    }

    /**
     * Register any package services.
     *
     * @return void
     */
    public function register()
    {
        $this->mergeConfigFrom(
            __DIR__ . '/../config/datadog-helper.php', 'datadog-helper'
        );

        \Datadogstatsd::configure(
            config('datadog-helper.api_key'),
            config('datadog-helper.application_key'),
            config('datadog-helper.datadog_host'),
            config('datadog-helper.transport'),
            config('datadog-helper.statsd_server'),
            config('datadog-helper.statsd_port')
        );

        $this->app->singleton('datadog', LaravelDatadogHelper::class);
    }
}


<?php

/*
 * This file is part of the h4cc/multipart package.
 *
 * (c) Julius Beckmann <github@h4cc.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace h4cc\Multipart;

use h4cc\Multipart\Parser\MultipartParser;

/**
 * Facade for parsing and selecting the right Multipart Parser.
 */
class ParserSelector
{
    /**
     * Give me your Content-Type, and i give you a parser.
     *
     * @param $contentType
     * @return MultipartParser|null
     */
    public function getParserForContentType($contentType)
    {
        if(0 !== stripos($contentType, 'multipart/')) {
            return null;
        }

        list($mime, $boundary) = $this->parseContentType($contentType);

        $parser = new MultipartParser();
        $parser->setBoundary($boundary);

        return $parser;
    }

    /**
     * Helper or parsing the Content-Type.
     *
     * @param $contentType
     * @return array
     * @throws ParserException
     */
    protected function parseContentType($contentType)
    {
        if(false === stripos($contentType, ';')) {
            throw new ParserException('ContentType does not contain a \';\'');
        }

        list($mime, $boundary) = explode(';', $contentType, 2);
        list($key, $boundaryValue) = explode('=', trim($boundary), 2);

        if('boundary' != $key) {
            throw new ParserException('Boundary does not start with \'boundary=\'');
        }

        return [strtolower(trim($mime)), $boundaryValue];
    }
}


<?php

/*
 * This file is part of the h4cc/multipart package.
 *
 * (c) Julius Beckmann <github@h4cc.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace h4cc\Multipart\Parser;

use h4cc\Multipart\ParserInterface;

/**
 * Some reuseable default functionality for multipart parser.
 */
abstract class AbstractParser implements ParserInterface
{
    protected $boundary;

    public function setBoundary($boundary)
    {
        $this->boundary = $boundary;
    }

    public function getBoundary()
    {
        return $this->boundary;
    }

    /**
     * Will parse a list of [header-]newline-body string.
     *
     * @param array $bodies
     * @return array
     */
    protected function parseBodies(array $bodies)
    {
        $parseBodies = [];

        foreach($bodies as $body) {
            $parseBodies[] = $this->parseBody($body);
        }

        return $parseBodies;
    }

    /**
     * Will parse a single [header-]newline-body string.
     *
     * @param $body
     * @return array
     */
    protected function parseBody($body)
    {
        // Headers come first, then content.
        $isHeader = true;
        $headers = [];
        $content = [];

        foreach (explode(static::EOL, $body) as $i => $line) {

            if (0 == $i) {
                // Skip the first line
                continue;
            }

            if ('' == trim($line)) {
                // First newline starts body in next line.
                $isHeader = false;
                continue;
            }

            if ($isHeader) {
                list($header, $value) = explode(':', $line);
                if ($header) {
                    $headers[strtolower($header)][] = trim($value);
                }
            } else {
                $content[] = $line;
            }
        }

        return ['headers' => $headers, 'body' => implode(static::EOL, $content)];
    }
}