Perforce Chronicle 2012.2/486814
API Documentation

P4Cms_View_Helper_HeadLink Class Reference

Aggregating version of HeadLink helper. More...

List of all members.

Public Member Functions

 __construct ()
 Extend parent constructor to add 'id' to valid item keys.
 getAssetHandler ()
 Get the asset handler used to store the aggregated css file(s).
 getDocumentRoot ()
 Get the file-system path to the document root.
 getQualifiedBaseUrl ()
 Retrieves the qualified base url.
 setAggregateCss ($aggregate)
 Enable or disable css aggregation.
 setAssetHandler (P4Cms_AssetHandlerInterface $handler=null)
 Set the asset handler used to store the aggregated css file(s).
 setDocumentRoot ($path)
 Set the file-system path to the document root.
 setIncludeTheme ($includeTheme)
 Enable or disable inclusion of theme links when rendering.
 toString ($indent=null)
 Extend toString to aggregate css files where possible and ensure that theme links appear last, for CSS precedence.

Protected Member Functions

 _aggregateCss ()
 Aggregate local, unconditional css files by media type.
 _canGzipCompress ()
 Check if this PHP can generate gzip compressed data.
 _clientAcceptsGzip ()
 Check if the client can accept gzip encoded content.
 _consolidateForInternetExplorer ()
 Internet Explorer limits the number of CSS files that can be linked to 32.
 _isDuplicateStylesheet ($uri)
 Is the linked stylesheet a duplicate? Extended to protect against empty 'rel' property.
 _minifyCss ($content)
 Minify CSS Strips comments and unnecessary whitespace.
 _resolveCssUrls ($content, $file)
 Resolve relative URLs in the given css content against the document root.
 _stripCharset ($content)
 Remove declarations from file for standards compliance (only one such declaration may appear in a stylesheet and it must be first line - aggregation breaks these rules).

Protected Attributes

 $_aggregate = false
 $_aggregated = false
 $_assetHandler = null
 $_documentRoot = null
 $_includeTheme = true

Detailed Description

Aggregating version of HeadLink helper.

Combines css files (resolving relative urls). Ensures that theme links come last.

Copyright:
2011-2012 Perforce Software. All rights reserved
License:
Please see LICENSE.txt in top-level folder of this distribution.
Version:
2012.2/486814

Constructor & Destructor Documentation

P4Cms_View_Helper_HeadLink::__construct ( )

Extend parent constructor to add 'id' to valid item keys.

    {
        parent::__construct();
        $this->_itemKeys[] = "id";
    }

Member Function Documentation

P4Cms_View_Helper_HeadLink::_aggregateCss ( ) [protected]

Aggregate local, unconditional css files by media type.

Resolves relative urls and concatenates css files into build files. Rebuilds whenever a file changes.

    {
        // bail out if no asset handler is configured
        if (!$this->getAssetHandler()) {
            P4Cms_Log::log(
                "Failed to aggregate CSS. Asset Handler is unset.",
                P4Cms_Log::ERR
            );
            return;
        }

        // bail out if document root is unset.
        if (!$this->getDocumentRoot()) {
            P4Cms_Log::log(
                "Failed to aggregate CSS. Document root is unset.",
                P4Cms_Log::ERR
            );
            return;
        }

        // group css files by media.
        $ignore = array();
        $groups = array();
        foreach ($this as $item) {

            // only aggregate CSS links that are local.
            if ($item->type !== 'text/css' || P4Cms_Uri::hasScheme($item->href)) {
                $ignore[] = $item;
                continue;
            }

            // ignore if file does not exist.
            $file = $this->getDocumentRoot() . $item->href;
            if (!file_exists($file)) {
                $ignore[] = $item;
                continue;
            }

            // group files by media, build group, and conditional files.
            // build groups are named collections of css files that should be
            // aggregated together
            $parts = array($item->media);
            if (isset($item->extras['buildGroup'])) {
                $parts[] = $item->extras['buildGroup'];
            }
            $parts[] = $item->conditionalStylesheet;
            $name = join('-', $parts);
            $name = preg_replace('/[^a-zA-Z0-9 .-]/', '', $name);
            $name = str_replace(' ', '-', $name);
            if (!isset($groups[$name])) {
                $groups[$name] = array(
                    'time'          => 0,
                    'media'         => $item->media,
                    'files'         => array(),
                    'conditional'   => $item->conditionalStylesheet
                );
            }

            // add link to the group.
            $time                       = filemtime($file);
            $groups[$name]['files'][]   = $file;
            $groups[$name]['time']      = $time < $groups[$name]['time']
                ? $groups[$name]['time']
                : $time;
        }

        // determine if compression should be enabled
        $compressed = $this->_canGzipCompress() && $this->_clientAcceptsGzip();

        // process build groups.
        $styles = array();
        foreach ($groups as $name => $group) {
            // generate build filename.
            $buildFile = $name . "-" . md5(implode(',', $group['files']) . $group['time']) 
                       . ($compressed ? '.cssgz' : '.css');

            // rebuild if file does not exist.
            if (!$this->getAssetHandler()->exists($buildFile)) {
                $css = "";
                foreach ($group['files'] as $file) {
                    $content = file_get_contents($file);
                    $content = $this->_stripCharset($content);
                    $content = $this->_minifyCss($content);
                    $content = $this->_resolveCssUrls($content, $file);
                    $css    .= $content;
                }

                // also compress if possible.
                if ($compressed) {
                    $css = gzencode($css, 9);
                }

                // write out aggregate file - if any fail, abort aggregation.
                if (!$this->getAssetHandler()->put($buildFile, $css)) {
                    return;
                }
            }

            // we just capture the stylesheet at this point as we want
            // to cleanly abort aggregation if any stylesheets fail.
            $styles[] = array(
                'uri'         => $this->getAssetHandler()->uri($buildFile), 
                'media'       => $group['media'],
                'conditional' => $group['conditional']
            );
        }

        // if we made it this far aggregation worked; update the
        // list to only contain ignored and aggregated links.
        $this->getContainer()->exchangeArray($ignore);
        foreach ($styles as $style) {
            $this->appendStylesheet($style['uri'], $style['media'], $style['conditional']);
        }

        $this->_aggregated = true;
    }
P4Cms_View_Helper_HeadLink::_canGzipCompress ( ) [protected]

Check if this PHP can generate gzip compressed data.

Returns:
bool true if this PHP has gzip support.
    {
        return function_exists('gzencode');
    }
P4Cms_View_Helper_HeadLink::_clientAcceptsGzip ( ) [protected]

Check if the client can accept gzip encoded content.

Returns:
bool true if the client supports gzip; false otherwise.
    {
        $front   = Zend_Controller_Front::getInstance();
        $request = $front->getRequest();
        $accepts = isset($request)
            ? $request->getHeader('Accept-Encoding')
            : '';

        return strpos($accepts, 'gzip') !== false;
    }
P4Cms_View_Helper_HeadLink::_consolidateForInternetExplorer ( ) [protected]

Internet Explorer limits the number of CSS files that can be linked to 32.

See the MSDN blog post: http://blogs.msdn.com/b/ieinternals/archive/2011/05/14/10164546.aspx It is possible to work around this limitation by using the CSS pragma.

This method emits one or more style blocks containing sufficient directives to ensure that all styles get loaded. However, nothing is done if the CSS has already been aggregated or the requesting browser does not appear to be MSIE.

    {
        // refuse to do anything for non-MSIE browsers
        $userAgent = array_key_exists('HTTP_USER_AGENT', $_SERVER)
            ? $_SERVER['HTTP_USER_AGENT']
            : '';
        if (!preg_match('/^Mozilla.+MSIE ([0-9]+[\.0-9]*)/', $userAgent)) {
            return '';
        }

        // group css files by media.
        $notConsolidated = array();
        $groups = array();
        foreach ($this as $item) {

            // only consolidate CSS.
            if ($item->type !== 'text/css') {
                $notConsolidated[] = $item;
                continue;
            }

            // group files by media and conditional stylesheet
            $name = !empty($item->conditionalStylesheet)
                ? $item->media . '-' . $item->conditionalStylesheet
                : $item->media;
            $name = preg_replace('/[^a-zA-Z0-9 .-]/', '', $name);
            if (!isset($groups[$name])) {
                $groups[$name] = array(
                    'urls'          => array(),
                    'attributes'    => array(
                        'media'         => $item->media,
                        'conditional'   => $item->conditionalStylesheet
                    )
                );
            }

            $groups[$name]['urls'][]   = $item->href;
        }

        // only keep ignored links in the list.
        $this->getContainer()->exchangeArray($notConsolidated);

        // process media groups and construct headStyle content featuring @import statements.
        // we clone the headLink helper, give it a fresh container, and render its output here
        // to avoid potential issues with/without headStyle execution elsewhere.
        $headStyle = clone $this->view->getHelper('headStyle');
        $headStyle->setContainer(new Zend_View_Helper_Placeholder_Container);
        foreach ($groups as $name => $group) {
            // batch stylesheets up to 31 at a time to meet MSIE limitations.
            while (count($group['urls'])) {
                $batch = array_splice($group['urls'], 0, 31);
                $styles = "@import url('" . join("');\n@import url('", $batch) ."');";
                $headStyle->appendStyle($styles, $group['attributes']);
            }
        }
        return $headStyle->toString();
    }
P4Cms_View_Helper_HeadLink::_isDuplicateStylesheet ( uri) [protected]

Is the linked stylesheet a duplicate? Extended to protect against empty 'rel' property.

Parameters:
string$uriStyle sheet
Returns:
bool
    {
        foreach ($this->getContainer() as $item) {
            if (isset($item->rel) && ($item->rel == 'stylesheet') && ($item->href == $uri)) {
                return true;
            }
        }
        return false;
    }
P4Cms_View_Helper_HeadLink::_minifyCss ( content) [protected]

Minify CSS Strips comments and unnecessary whitespace.

Parameters:
string$contentthe css to minify.
Returns:
string the input css with comments removed.
    {
        // strip comments.
        $content = preg_replace('#/\*.*\*/#Us', '', $content);

        // strip needless whitespace.
        $content = preg_replace('#\s*([\s{},:;])\s*#s', '\\1', $content);

        return $content;
    }
P4Cms_View_Helper_HeadLink::_resolveCssUrls ( content,
file 
) [protected]

Resolve relative URLs in the given css content against the document root.

This ensures the links continue to work post aggregation.

Parameters:
string$contentthe css to resolve links in.
string$filethe original location of the css file.
Returns:
string the css with relative URLs replace.
    {
        $baseUrl  = $this->getAssetHandler()->isOffsite() ? $this->getQualifiedBaseUrl() : '';
        $basePath = $baseUrl . str_replace($this->getDocumentRoot(), '', dirname($file));
        return preg_replace_callback(
            '/url\([\'"]?([^\'")]+)[\'"]?\)/i',
            function ($matches) use ($baseUrl, $basePath)
            {
                // if it is a full url with schema just return as-is
                if (P4Cms_Uri::hasScheme($matches[1])) {
                    return $matches[0];
                }

                // if it isn't relative, but is lacking a schema, glue in the baseUrl
                if (!P4Cms_Uri::isRelativeUri($matches[1])) {
                    return "url('" . $baseUrl . "/" . $matches[1] . "')";
                }

                // if it's relative, glue in base path (which includes baseUrl)
                return "url('" . $basePath . "/" . $matches[1] . "')";
            },
            $content
        );
    }
P4Cms_View_Helper_HeadLink::_stripCharset ( content) [protected]

Remove declarations from file for standards compliance (only one such declaration may appear in a stylesheet and it must be first line - aggregation breaks these rules).

Parameters:
string$contentthe css to strip 's from.
Returns:
string the css with 's removed.
    {
        return preg_replace(
            '/^@charset\s+[\'"](\S*)\b[\'"];/i',
            '',
            $content
        );
    }
P4Cms_View_Helper_HeadLink::getAssetHandler ( )

Get the asset handler used to store the aggregated css file(s).

Returns:
P4Cms_AssetHandlerInterface|null the handler to use or null
    {
        return $this->_assetHandler;
    }
P4Cms_View_Helper_HeadLink::getDocumentRoot ( )

Get the file-system path to the document root.

Returns:
string the location of the public folder.
    {
        return $this->_documentRoot;
    }
P4Cms_View_Helper_HeadLink::getQualifiedBaseUrl ( )

Retrieves the qualified base url.

Returns:
string|null The qualified base url in use
    {
        $request = Zend_Controller_Front::getInstance()->getRequest();
        if (!$request instanceof Zend_Controller_Request_Http) {
            throw new Zend_View_Exception(
                "Cannot assemble qualified base URL - not an http request."
            );
        }

        return $request->getScheme() . "://" . $request->getHttpHost() . $request->getBaseUrl();
    }
P4Cms_View_Helper_HeadLink::setAggregateCss ( aggregate)

Enable or disable css aggregation.

Parameters:
bool$aggregateset to true to enable, false to disable.
Returns:
P4Cms_View_Helper_HeadLink provides fluent interface.
    {
        $this->_aggregate = (bool) $aggregate;
        return $this;
    }
P4Cms_View_Helper_HeadLink::setAssetHandler ( P4Cms_AssetHandlerInterface handler = null)

Set the asset handler used to store the aggregated css file(s).

Parameters:
P4Cms_AssetHandlerInterface | null$handlerThe handler to use or null
Returns:
P4Cms_View_Helper_Dojo_Container provides fluent interface.
    {
        $this->_assetHandler = $handler;

        return $this;
    }
P4Cms_View_Helper_HeadLink::setDocumentRoot ( path)

Set the file-system path to the document root.

Parameters:
string$paththe location of the public folder.
Returns:
P4Cms_View_Helper_HeadLink provides fluent interface.
    {
        $this->_documentRoot = rtrim($path, '/\\');
        return $this;
    }
P4Cms_View_Helper_HeadLink::setIncludeTheme ( includeTheme)

Enable or disable inclusion of theme links when rendering.

In some display contexts it might be desirable to disable theme stylesheets so that they don't influence presentation.

Parameters:
bool$includeThemetrue to include theme links (the default) false to exclude them
Returns:
P4Cms_View_Helper_HeadLink provides fluent interface
    {
        $this->_includeTheme = (bool) $includeTheme;
        
        return $this;
    }
P4Cms_View_Helper_HeadLink::toString ( indent = null)

Extend toString to aggregate css files where possible and ensure that theme links appear last, for CSS precedence.

Parameters:
string | int$indentZend provides no documentation for this param.
Returns:
string
    {
        $themePath = P4Cms_Theme::fetchActive()->getBaseUrl();
        $items     = array();
        $themes    = array();
        foreach ($this as $item) {
            if (strpos($item->href, $themePath) === false) {
                $items[]  = $item;
            } else if ($this->_includeTheme) {
                $themes[] = $item;
            }
        }
        $this->getContainer()->exchangeArray(array_merge($items, $themes));

        // aggregate css files (but only once).
        if ($this->_aggregate && !$this->_aggregated) {
            $this->_aggregateCss();
        }

        // when aggregation is not enabled, consolidate MSIE stylesheets which limits CSS links to 32.
        $consolidated = '';
        if (!$this->_aggregated) {
            $consolidated = $this->_consolidateForInternetExplorer();
        }

        return $consolidated . parent::toString();
    }

Member Data Documentation

P4Cms_View_Helper_HeadLink::$_aggregate = false [protected]
P4Cms_View_Helper_HeadLink::$_aggregated = false [protected]
P4Cms_View_Helper_HeadLink::$_assetHandler = null [protected]
P4Cms_View_Helper_HeadLink::$_documentRoot = null [protected]
P4Cms_View_Helper_HeadLink::$_includeTheme = true [protected]

The documentation for this class was generated from the following file: