Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Refactor: renamed Blog module to PhlyBlog

- In preparation for extracting into its own repository
  • Loading branch information...
commit 95cd1bc166efc64a354f2b467951e8ac2605e4ad 1 parent a3f23fd
@weierophinney weierophinney authored
Showing with 3,086 additions and 0 deletions.
  1. +49 −0 Module.php
  2. +182 −0 README.md
  3. +14 −0 autoload_classmap.php
  4. +12 −0 autoload_function.php
  5. +2 −0  autoload_register.php
  6. +82 −0 bin/compile.php
  7. +166 −0 config/module.config.php
  8. +29 −0 misc/sample-post.php
  9. +119 −0 public/css/phly-blog.css
  10. +8 −0 public/js/blog.js
  11. +561 −0 src/PhlyBlog/Compiler.php
  12. +10 −0 src/PhlyBlog/Compiler/FileWriter.php
  13. +72 −0 src/PhlyBlog/Compiler/PhpFileFilter.php
  14. +39 −0 src/PhlyBlog/Compiler/SortedEntries.php
  15. +7 −0 src/PhlyBlog/Compiler/WriterInterface.php
  16. +554 −0 src/PhlyBlog/CompilerOptions.php
  17. +608 −0 src/PhlyBlog/EntryEntity.php
  18. +50 −0 src/PhlyBlog/Filter/EntryFilter.php
  19. +20 −0 src/PhlyBlog/Filter/Permalink.php
  20. +34 −0 src/PhlyBlog/Filter/Tags.php
  21. +256 −0 test/PhlyBlog/EntryEntityTest.php
  22. +16 −0 test/bootstrap.php
  23. +17 −0 test/bootstrap.php.dist
  24. 0  test/log/.placeholder
  25. +15 −0 test/phpunit.xml
  26. +26 −0 view/phly-blog/entry-short.phtml
  27. +49 −0 view/phly-blog/entry.phtml
  28. +33 −0 view/phly-blog/list.phtml
  29. +42 −0 view/phly-blog/paginator.phtml
  30. +14 −0 view/phly-blog/tags.phtml
View
49 Module.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace PhlyBlog;
+
+use Traversable,
+ Zend\EventManager\StaticEventManager,
+ Zend\Module\Consumer\AutoloaderProvider,
+ Zend\Stdlib\ArrayUtils;
+
+class Module implements AutoloaderProvider
+{
+ public static $config;
+
+ public function getAutoloaderConfig()
+ {
+ return array(
+ 'Zend\Loader\ClassMapAutoloader' => array(
+ __DIR__ . '/autoload_classmap.php'
+ ),
+ );
+ }
+
+ public function getConfig()
+ {
+ return include __DIR__ . '/config/module.config.php';
+ }
+
+ public function init($manager)
+ {
+ $events = StaticEventManager::getInstance();
+ $events->attach('bootstrap', 'bootstrap', array($this, 'onBootstrap'));
+ }
+
+ public function onBootstrap($e)
+ {
+ self::$config = $e->getParam('config');
+ if (self::$config instanceof Traversable) {
+ self::$config = ArrayUtils::iteratorToArray(self::$config);
+ }
+ }
+
+ public static function prepareCompilerView($view, $config, $locator)
+ {
+ $renderer = $locator->get('Zend\View\Renderer\PhpRenderer');
+ $view->addRenderingStrategy(function($e) use ($renderer) {
+ return $renderer;
+ }, 100);
+ }
+}
View
182 README.md
@@ -0,0 +1,182 @@
+Static Blog
+===========
+
+This module is a tool for generating a static blog.
+
+Blog posts are simply PHP files that create and return `Blog\EntryEntity`
+objects. You point the compiler at a directory, and it creates a tree of files
+representing your blog and its feeds. These can either be consumed by your
+application, or they can be plain old HTML markup files that you serve
+directly.
+
+Requirements
+----
+
+* PHP >= 5.3.3
+* Zend Framework 2 >= 2.0.0beta3, specifically:
+ * Zend\View\View, used to render and write generated files
+ * Zend\Mvc and Zend\Module, as this implements a module, and the compiler
+ script depends on it and an Application instance. As such, it also has
+ a dependency on Zend\Loader, Zend\Di, and Zend\EventManager.
+ * Zend\Feed\Writer
+ * Zend\Tag\Cloud
+
+Writing Entries
+====
+
+Find a location in your repository for entries, preferably outside your document
+root; I recommend either `data/blog/` or `posts/`.
+
+Post files are simply PHP files that return a `Blog\EntryEntity` instance. A
+sample is provided in `misc/sample-post.php`. This post can be copied as a
+template.
+
+Important things to note:
+
+* Set the created and/or updated timestamps. Alternately, use `DateTime` or
+ `date()` to generate a timestamp based on a date/time string.
+* Entries marked as "drafts" (i.e., `setDraft(true)`) will not be published.
+* Entries marked as private (i.e., `setPublic(false)`) will be published, but
+ will not be aggregated in paginated views or feeds. As such, you need to hand
+ the URL to somebody in order for them to see it.
+* You can set an array of tags. Tags can have whitespace, which will be
+ translated to "+" characters.
+
+Usage
+=====
+
+A script, `bin/compile.php`, is shipped for your convenience, and it will
+generate the following artifacts:
+
+* A file per entry
+* Paginated entry files
+* Paginated entry files by year
+* Paginated entry files by month
+* Paginated entry files by day
+* Paginated entry files by tag
+* Atom and/or RSS feeds for recent entries
+* Atom and/or RSS feeds for recent entries by tag
+* Optionally, a tag cloud
+
+The script, makes the following assumptions:
+
+* They are being called by another script that:
+ * sets up one or more autoloaders, including functionality to autoload the
+ code in this library
+ * compiles and merges all application configuration
+ * bootstraps the application
+ * retains the Application instance in the current scope
+
+Basically, a script that does normal bootstrapping, but without calling `run()`
+or `send()` on the Application instance.
+
+You will want to setup local configuration; I recommend putting it in
+`config/autoload/module.blog.config.global.php`. As a sample:
+
+ ```php
+ <?php
+ return array(
+ 'blog' => array(
+ 'options' => array(
+ // The following indicate where to write files. Note that this
+ // configuration writes to the "public/" directory, which would
+ // create a blog made from static files. For the various
+ // paginated views, "%d" is the current page number; "%s" is
+ // typically a date string (see below for more information) or tag.
+ 'by_day_filename_template' => 'public/blog/day/%s-p%d.html',
+ 'by_month_filename_template' => 'public/blog/month/%s-p%d.html',
+ 'by_tag_filename_template' => 'public/blog/tag/%s-p%d.html',
+ 'by_year_filename_template' => 'public/blog/year/%s-p%d.html',
+ 'entries_filename_template' => 'public/blog-p%d.html',
+
+ // In this case, the "%s" is the entry ID.
+ 'entry_filename_template' => 'public/blog/%s.html',
+
+ // For feeds, the final "%s" is the feed type -- "atom" or "rss". In
+ // the case of the tag feed, the initial "%s" is the current tag.
+ 'feed_filename' => 'public/blog-%s.xml',
+ 'tag_feed_filename_template' => 'public/blog/tag/%s-%s.xml',
+
+ // This is the link to a blog post
+ 'entry_link_template' => '/blog/%s.html',
+
+ // These are the various URL templates for "paginated" views. The
+ // "%d" in each is the current page number.
+ 'entries_url_template' => '/blog-p%d.html',
+ // For the year/month/day paginated views, "%s" is a string
+ // representing the date. By default, this will be "YYYY",
+ // "YYYY/MM", and "YYYY/MM/DD", respectively.
+ 'by_year_url_template' => '/blog/year/%s-p%d.html',
+ 'by_month_url_template' => '/blog/month/%s-p%d.html',
+ 'by_day_url_template' => '/blog/day/%s-p%d.html',
+
+ // These are the primary templates you will use -- the first is for
+ // paginated lists of entries, the second for individual entries.
+ // There are of course more templates, but these are the only ones
+ // that will be directly referenced and rendered by the compiler.
+ 'entries_template' => 'blog/list',
+ 'entry_template' => 'blog/entry',
+
+ 'feed_author_email' => 'me@mwop.net',
+ 'feed_author_name' => "Matthew Weier O'Phinney",
+ 'feed_author_uri' => 'http://mwop.net',
+ 'feed_hostname' => 'http://mwop.net',
+ 'feed_title' => 'Blog Entries :: phly, boy, phly',
+ 'tag_feed_title_template' => 'Tag: %s :: phly, boy, phly',
+
+ // If generating a tag cloud, you can specify options for
+ // Zend\Tag\Cloud. The following sets up percentage sizing from
+ // 80-300%
+ 'tag_cloud_options' => array('tagDecorator' => array(
+ 'decorator' => 'html_tag',
+ 'options' => array(
+ 'fontSizeUnit' => '%',
+ 'minFontSize' => 80,
+ 'maxFontSize' => 300,
+ ),
+ )),
+ ),
+
+ // This is the location where you are keeping your post files (the PHP
+ // files returning `Blog\EntryEntity` objects).
+ 'posts_path' => 'data/posts/',
+
+ // You can provide your own callback to setup renderer and response
+ // strategies. This is useful, for instance, for injecting your
+ // rendered contents into a layout.
+ // The callback will receive a View instance, application configuration
+ // (as an array), and the application's Locator instance.
+ 'view_callback' => array('Application\Module', 'prepareCompilerView'),
+
+ // Tag cloud generation is possible, but you likely need to capture
+ // the rendered cloud to inject elsewhere. You can do this with a
+ // callback.
+ // The callback will receive a Zend\Tag\Cloud instance, the View
+ // instance, application configuration // (as an array), and the
+ // application's Locator instance.
+ 'cloud_callback' => array('Application\Module', 'handleTagCloud'),
+ ),
+ 'di' => array('instance' => array(
+ // You will likely want to customize the templates provided. Do so by
+ // creating your own in your own module, and make sure you alter the
+ // resolvers so that they point to the override locations. Below, I'm
+ // putting my overrides in my Application module.
+ 'Zend\View\Resolver\TemplateMapResolver' => array('parameters' => array(
+ 'map' => array(
+ 'blog/entry-short' => 'module/Application/view/blog/entry-short.phtml',
+ 'blog/entry' => 'module/Application/view/blog/entry.phtml',
+ 'blog/list' => 'module/Application/view/blog/list.phtml',
+ 'blog/paginator' => 'module/Application/view/blog/paginator.phtml',
+ 'blog/tags' => 'module/Application/view/blog/tags.phtml',
+ ),
+ )),
+
+ 'Zend\View\Resolver\TemplatePathStack' => array('parameters' => array(
+ 'paths' => array(
+ 'blog' => 'module/Application/view',
+ ),
+ )),
+ ));
+
+When you run the script, it will generate files in the locations you specify in
+your configuration.
View
14 autoload_classmap.php
@@ -0,0 +1,14 @@
+<?php
+// Generated by ZF2's ./bin/classmap_generator.php
+return array(
+ 'PhlyBlog\Filter\EntryFilter' => __DIR__ . '/src/PhlyBlog/Filter/EntryFilter.php',
+ 'PhlyBlog\Filter\Tags' => __DIR__ . '/src/PhlyBlog/Filter/Tags.php',
+ 'PhlyBlog\Filter\Permalink' => __DIR__ . '/src/PhlyBlog/Filter/Permalink.php',
+ 'PhlyBlog\Compiler\WriterInterface' => __DIR__ . '/src/PhlyBlog/Compiler/WriterInterface.php',
+ 'PhlyBlog\Compiler\FileWriter' => __DIR__ . '/src/PhlyBlog/Compiler/FileWriter.php',
+ 'PhlyBlog\Compiler\SortedEntries' => __DIR__ . '/src/PhlyBlog/Compiler/SortedEntries.php',
+ 'PhlyBlog\Compiler\PhpFileFilter' => __DIR__ . '/src/PhlyBlog/Compiler/PhpFileFilter.php',
+ 'PhlyBlog\Compiler' => __DIR__ . '/src/PhlyBlog/Compiler.php',
+ 'PhlyBlog\CompilerOptions' => __DIR__ . '/src/PhlyBlog/CompilerOptions.php',
+ 'PhlyBlog\EntryEntity' => __DIR__ . '/src/PhlyBlog/EntryEntity.php',
+);
View
12 autoload_function.php
@@ -0,0 +1,12 @@
+<?php
+return function ($class) {
+ static $map;
+ if (!$map) {
+ $map = include __DIR__ . '/autoload_classmap.php';
+ }
+
+ if (!isset($map[$class])) {
+ return false;
+ }
+ return include $map[$class];
+};
View
2  autoload_register.php
@@ -0,0 +1,2 @@
+<?php
+spl_autoload_register(include __DIR__ . '/autoload_function.php');
View
82 bin/compile.php
@@ -0,0 +1,82 @@
+<?php
+use PhlyBlog\Module;
+use PhlyBlog\Compiler;
+use PhlyBlog\CompilerOptions;
+use Zend\Loader\AutoloaderFactory;
+use Zend\Module\Listener;
+use Zend\Module\Manager as ModuleManager;
+use Zend\Mvc\Application;
+use Zend\Mvc\Bootstrap;
+use Zend\View\Model\ViewModel;
+
+// Get locator, and grab renderer and view from it
+$config = Module::$config;
+$locator = $application->getLocator();
+$view = $locator->get('Zend\View\View');
+$view->events()->clearListeners('renderer');
+$view->events()->clearListeners('response');
+
+// Setup renderer for layout, and layout view model
+if ($config['blog']['view_callback'] && is_callable($config['blog']['view_callback'])) {
+ $callable = $config['blog']['view_callback'];
+ call_user_func($callable, $view, $config, $locator);
+}
+
+// Prepare compiler and grab tag cloud
+$options = new CompilerOptions($config['blog']['options']);
+$postFiles = new Compiler\PhpFileFilter($config['blog']['posts_path']);
+$writer = new Compiler\FileWriter();
+$compiler = new Compiler($postFiles, $view, $writer, $options);
+
+// Create tag cloud
+if ($config['blog']['cloud_callback']
+ && is_callable($config['blog']['cloud_callback'])
+) {
+ $callable = $config['blog']['cloud_callback'];
+ echo "Creating and rendering tag cloud...";
+ $cloud = $compiler->compileTagCloud();
+ call_user_func($callable, $cloud, $view, $config, $locator);
+ echo "DONE!\n";
+}
+
+// compile!
+
+echo "Compiling paginated entries...";
+$compiler->compilePaginatedEntries();
+echo "DONE!\n";
+
+echo "Compiling paginated entries by year...";
+$compiler->compilePaginatedEntriesByYear();
+echo "DONE!\n";
+
+echo "Compiling paginated entries by month...";
+$compiler->compilePaginatedEntriesByMonth();
+echo "DONE!\n";
+
+echo "Compiling paginated entries by date...";
+$compiler->compilePaginatedEntriesByDate();
+echo "DONE!\n";
+
+echo "Compiling paginated entries by tag...";
+$compiler->compilePaginatedEntriesByTag();
+echo "DONE!\n";
+
+echo "Compiling entries...";
+$compiler->compileEntryViewScripts();
+echo "DONE!\n";
+
+echo "Compiling main Atom feed...";
+$compiler->compileRecentFeed('atom');
+echo "DONE!\n";
+
+echo "Compiling main RSS feed...";
+$compiler->compileRecentFeed('rss');
+echo "DONE!\n";
+
+echo "Compiling Atom tag feeds...";
+$compiler->compileTagFeeds('atom');
+echo "DONE!\n";
+
+echo "Compiling RSS tag feeds...";
+$compiler->compileTagFeeds('rss');
+echo "DONE!\n";
View
166 config/module.config.php
@@ -0,0 +1,166 @@
+<?php
+$config = array();
+
+$config['blog'] = array(
+ 'options' => array(
+ 'by_day_filename_template' => 'public/blog/day/%s-p%d.html',
+ 'by_month_filename_template' => 'public/blog/month/%s-p%d.html',
+ 'by_tag_filename_template' => 'public/blog/tag/%s-p%d.html',
+ 'by_year_filename_template' => 'public/blog/year/%s-p%d.html',
+ 'entries_filename_template' => 'public/blog-p%d.html',
+ 'entries_template' => 'phly-blog/list',
+ 'entry_filename_template' => 'public/blog/%s.html',
+ 'entry_link_template' => '/blog/%s.html',
+ 'entry_template' => 'phly-blog/entry',
+ 'feed_author_email' => 'you@your.tld',
+ 'feed_author_name' => "Your name here",
+ 'feed_author_uri' => 'http://your.tld',
+ 'feed_filename' => 'public/blog-%s.xml',
+ 'feed_hostname' => 'http://your.tld',
+ 'feed_title' => 'Blog Entries',
+ 'tag_feed_filename_template' => 'public/blog/tag/%s-%s.xml',
+ 'tag_feed_title_template' => 'Tag: %s',
+ 'tag_cloud_options' => array('tagDecorator' => array(
+ 'decorator' => 'html_tag',
+ 'options' => array(
+ 'fontSizeUnit' => '%',
+ 'minFontSize' => 80,
+ 'maxFontSize' => 300,
+ ),
+ )),
+ ),
+ 'posts_path' => 'data/blog/',
+ 'view_callback' => array('PhlyBlog\Module', 'prepareCompilerView'),
+ 'cloud_callback' => false,
+);
+
+$config['di'] = array(
+'instance' => array(
+ 'Zend\View\Resolver\TemplateMapResolver' => array('parameters' => array(
+ 'map' => array(
+ 'phly-blog/entry-short' => __DIR__ . '/../view/phly-blog/entry-short.phtml',
+ 'phly-blog/entry' => __DIR__ . '/../view/phly-blog/entry.phtml',
+ 'phly-blog/list' => __DIR__ . '/../view/phly-blog/list.phtml',
+ 'phly-blog/paginator' => __DIR__ . '/../view/phly-blog/paginator.phtml',
+ 'phly-blog/tags' => __DIR__ . '/../view/phly-blog/tags.phtml',
+ ),
+ )),
+
+ 'Zend\View\Resolver\TemplatePathStack' => array('parameters' => array(
+ 'paths' => array(
+ 'phly-blog' => __DIR__ . '/../view',
+ ),
+ )),
+
+ /* Routes are only included to simplify url creation */
+ 'Zend\Mvc\Router\RouteStack' => array('parameters' => array(
+ 'routes' => array(
+ 'phly-blog' => array(
+ 'type' => 'Literal',
+ 'options' => array(
+ 'route' => '/blog',
+ ),
+ 'may_terminate' => false,
+ 'child_routes' => array(
+ 'index' => array(
+ 'type' => 'Literal',
+ 'options' => array(
+ 'route' => '.html',
+ ),
+ ),
+ 'feed-atom' => array(
+ 'type' => 'Literal',
+ 'options' => array(
+ 'route' => '-atom.xml',
+ ),
+ ),
+ 'feed-rss' => array(
+ 'type' => 'Literal',
+ 'options' => array(
+ 'route' => '-rss.xml',
+ ),
+ ),
+ 'entry' => array(
+ 'type' => 'Regex',
+ 'options' => array(
+ 'regex' => '/(?<id>[^/]+)\.html',
+ 'spec' => '/%id%.html',
+ ),
+ ),
+ 'tag' => array(
+ 'type' => 'Regex',
+ 'options' => array(
+ 'regex' => '/tag/(?<tag>[^/.-]+)',
+ 'defaults' => array(
+ 'action' => 'tag',
+ ),
+ 'spec' => '/tag/%tag%',
+ ),
+ 'may_terminate' => false,
+ 'child_routes' => array(
+ 'page' => array(
+ 'type' => 'Literal',
+ 'options' => array(
+ 'route' => '.html',
+ ),
+ ),
+ 'feed-atom' => array(
+ 'type' => 'Literal',
+ 'options' => array(
+ 'route' => '-atom.xml',
+ ),
+ ),
+ 'feed-ress' => array(
+ 'type' => 'Literal',
+ 'options' => array(
+ 'route' => '-ress.xml',
+ ),
+ ),
+ ),
+ ),
+ 'year' => array(
+ 'type' => 'Segment',
+ 'options' => array(
+ 'route' => '/year/:year.html',
+ 'constraints' => array(
+ 'year' => '\d{4}',
+ ),
+ 'defaults' => array(
+ 'action' => 'year',
+ ),
+ ),
+ ),
+ 'month' => array(
+ 'type' => 'Segment',
+ 'options' => array(
+ 'route' => '/month/:year/:month.html',
+ 'constraints' => array(
+ 'year' => '\d{4}',
+ 'month' => '\d{2}',
+ ),
+ 'defaults' => array(
+ 'action' => 'month',
+ ),
+ ),
+ ),
+ 'day' => array(
+ 'type' => 'Segment',
+ 'options' => array(
+ 'route' => '/day/:year/:month/:day.html',
+ 'constraints' => array(
+ 'year' => '\d{4}',
+ 'month' => '\d{2}',
+ 'day' => '\d{2}',
+ ),
+ 'defaults' => array(
+ 'action' => 'day',
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ )),
+));
+
+return $config;
View
29 misc/sample-post.php
@@ -0,0 +1,29 @@
+<?php
+use PhlyBlog\EntryEntity;
+
+$entry = new EntryEntity();
+
+$entry->setId('this-is-the-stub-used-in-the-uri-and-should-be-unique');
+$entry->setTitle('New site!');
+$entry->setAuthor('matthew');
+$entry->setDraft(false);
+$entry->setPublic(true);
+$entry->setCreated(1300744335);
+$entry->setUpdated(1301034313);
+$entry->setTimezone('America/New_York');
+$entry->setTags(array('php', 'personal'));
+
+$body =<<<'EOT'
+<p>
+ This is the principal body of the post, and will be shown everywhere.
+</p>
+EOT;
+$entry->setBody($body);
+
+$extended =<<<'EOT'
+This is the extended portion of the entry, and is only shown in the main entry
+views.
+EOT;
+$entry->setExtended($extended);
+
+return $entry;
View
119 public/css/phly-blog.css
@@ -0,0 +1,119 @@
+@import url(http://ajax.googleapis.com/ajax/libs/dojo/1.6/dojox/highlight/resources/pygments/autumn.css);
+@import url(http://ajax.googleapis.com/ajax/libs/dojo/1.6/dojox/highlight/resources/highlight.css);
+
+.cloud ul {
+ display: block;
+ text-align: left;
+}
+
+.cloud ul li {
+ display: inline;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+div.example {
+ font-family: 'Droid Sans Mono', arial, serif;
+ background-color: #F3F3F3;
+ padding: 3px;
+ margin: 1em;
+ border: 1px solid #000;
+ overflow: auto;
+ -moz-box-shadow: 3px 3px 4px #000;
+ -webkit-box-shadow: 3px 3px 4px #000;
+ box-shadow: 3px 3px 4px #000;
+ /* For IE 8 */
+ -ms-filter: "progid:DXImageTransform.Microsoft.Shadow(Strength=4, Direction=135, Color='#000000')";
+ /* For IE 5.5 - 7 */
+ filter: progid:DXImageTransform.Microsoft.Shadow(Strength=4, Direction=135, Color='#000000');
+}
+
+.blog .metadata {
+ background-color: #F3F3F3;
+ padding: 3px;
+ margin: 1em;
+ border: 1px solid #000;
+ overflow: auto;
+ -moz-box-shadow: 3px 3px 4px #000;
+ -webkit-box-shadow: 3px 3px 4px #000;
+ box-shadow: 3px 3px 4px #000;
+ /* For IE 8 */
+ -ms-filter: "progid:DXImageTransform.Microsoft.Shadow(Strength=4, Direction=135, Color='#000000')";
+ /* For IE 5.5 - 7 */
+ filter: progid:DXImageTransform.Microsoft.Shadow(Strength=4, Direction=135, Color='#000000');
+}
+
+.blog .metadata li {
+ padding-left: 0;
+ margin-left: 0;
+}
+
+
+.blog .entry.meta {
+ border-top: 3px solid #676767;
+ text-align: right;
+ background-color: #F3F3F3;
+}
+
+.blog .entry.meta p {
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+}
+
+.blog p.tags {
+ padding-bottom: 0.5em;
+}
+
+.blog p.date {
+ font-weight: bold;
+}
+
+.blog h4.entry {
+ border-top: 1px dotted #676767;
+ padding-top: 0.5em;
+}
+
+.blog .entry.body {
+ padding-left: 2em;
+}
+
+.social-media {
+ padding-bottom: 1em;
+}
+
+/* ensure we don't mess with disqus styles */
+
+#dsq-content #dsq-global-toolbar { clear: both !important; }
+
+.dsq-append-post {zoom: 1;}
+
+div#disqus_thread li, div#disqus_thread span {
+ list-style-type: none !important;
+}
+
+.dsq-comment, .dsq-comment-header, .dsq-comment-message, .dsq-comment-meta {
+ text-indent: 0;
+ padding-left: 0;
+}
+
+.dsq-comment-meta li, .dsq-comment-actions li {
+ text-indent: 0;
+}
+
+ul.dsq-comment-actions li {
+ display: inline;
+}
+
+.dsq-like-activity {
+ padding-left: 1em !important;
+}
+
+.dsq-toolbar-label {
+ padding-left: 1em !important;
+}
+
+#dsq-subscribe .dsq-font {
+ margin-left: 0.5em !important;
+ padding-left: 0.5em !important;
+}
View
8 public/js/blog.js
@@ -0,0 +1,8 @@
+dojo.provide("PhlyBlog.blog");
+
+dojo.require("dojox.highlight");
+dojo.require("dojox.highlight.languages._all");
+dojo.require("dojox.highlight.languages.pygments.css");
+dojo.ready(function() {
+ dojo.query("div.example pre code").forEach(dojox.highlight.init);
+});
View
561 src/PhlyBlog/Compiler.php
@@ -0,0 +1,561 @@
+<?php
+namespace PhlyBlog;
+
+use DateTime,
+ DateTimezone,
+ DomainException,
+ InvalidArgumentException,
+ Iterator,
+ stdClass,
+ Traversable,
+ Zend\Feed\Writer\Feed as FeedWriter,
+ Zend\Paginator\Paginator,
+ Zend\Paginator\Adapter\ArrayAdapter as ArrayPaginator,
+ Zend\Tag\Cloud as TagCloud,
+ Zend\View\Model\ViewModel,
+ Zend\View\View;
+
+class Compiler
+{
+ protected $byAuthor;
+ protected $byDay;
+ protected $byMonth;
+ protected $byTag;
+ protected $byYear;
+ protected $entries;
+ public $filename;
+ protected $files;
+ protected $options;
+ protected $responseStrategyPrepared = false;
+ protected $tagCloud;
+ protected $view;
+ protected $writer;
+
+ public function __construct(Compiler\PhpFileFilter $files, View $view, Compiler\WriterInterface $writer, CompilerOptions $options = null)
+ {
+ $this->files = $files;
+ $this->view = $view;
+ $this->writer = $writer;
+ if (null === $options) {
+ $options = new CompilerOptions;
+ }
+ $this->options = $options;
+ }
+
+ public function compileEntryViewScripts($template = null)
+ {
+ if (null === $template) {
+ $template = $this->options->getEntryTemplate();
+ if (empty($template)) {
+ throw new \DomainException('No template provided for individual entries');
+ }
+ }
+ $filenameTemplate = $this->options->getEntryFilenameTemplate();
+
+ $this->prepareEntries();
+
+ foreach ($this->entries as $entry) {
+ $filename = sprintf($filenameTemplate, $entry->getId());
+ $this->prepareResponseStrategy($filename);
+
+ $model = new ViewModel(array(
+ 'entry' => $entry,
+ ));
+ $model->setTemplate($template);
+
+ $this->view->render($model);
+ }
+ }
+
+ public function compilePaginatedEntries($template = null)
+ {
+ if (null === $template) {
+ $template = $this->options->getEntriesTemplate();
+ if (empty($template)) {
+ throw new \DomainException('No template provided for listing entries');
+ }
+ }
+ $filenameTemplate = $this->options->getEntriesFilenameTemplate();
+ $urlTemplate = $this->options->getEntriesUrlTemplate();
+ $title = $this->options->getEntriesTitle();
+
+ $this->prepareEntries();
+
+ $this->iterateAndRenderList(
+ $this->pagedEntries,
+ $filenameTemplate,
+ array(),
+ $title,
+ $urlTemplate,
+ false,
+ $template
+ );
+ }
+
+ public function compileRecentFeed($type, $title = '')
+ {
+ $type = strtolower($type);
+ if (!in_array($type, array('atom', 'rss'))) {
+ throw new InvalidArgumentException('Feed type must be "atom" or "rss"');
+ }
+
+ $filename = $this->options->getFeedFilename();
+ $blogLink = $this->options->getFeedBlogLink();
+ $feedLink = $this->options->getFeedFeedLink();
+ $title = $this->options->getFeedTitle();
+
+ $this->prepareEntries();
+
+ $this->iterateAndGenerateFeed(
+ $type,
+ $this->pagedEntries,
+ $title,
+ $blogLink,
+ $feedLink,
+ $filename
+ );
+ }
+
+ public function compilePaginatedEntriesByYear($template = null)
+ {
+ if (null === $template) {
+ $template = $this->options->getByYearTemplate();
+ if (empty($template)) {
+ throw new \DomainException('No template provided for listing entries by year');
+ }
+ }
+
+ $filenameTemplate = $this->options->getByYearFilenameTemplate();
+ $urlTemplate = $this->options->getByYearUrlTemplate();
+ $titleTemplate = $this->options->getByYearTitle();
+
+ $this->prepareEntries();
+ foreach ($this->byYear as $year => $list) {
+ $this->iterateAndRenderList(
+ $list,
+ $filenameTemplate,
+ array($year),
+ sprintf($titleTemplate, $year),
+ $urlTemplate,
+ $year,
+ $template
+ );
+ }
+ }
+
+ public function compilePaginatedEntriesByMonth($template = null)
+ {
+ if (null === $template) {
+ $template = $this->options->getByMonthTemplate();
+ if (empty($template)) {
+ throw new \DomainException('No template provided for listing entries by month');
+ }
+ }
+
+ $filenameTemplate = $this->options->getByMonthFilenameTemplate();
+ $urlTemplate = $this->options->getByMonthUrlTemplate();
+ $titleTemplate = $this->options->getByMonthTitle();
+
+ $this->prepareEntries();
+
+ foreach ($this->byMonth as $month => $list) {
+ // Get the year and month digits
+ list($year, $monthDigit) = explode('/', $month, 2);
+
+ $this->iterateAndRenderList(
+ $list,
+ $filenameTemplate,
+ array($month),
+ sprintf($titleTemplate, date('F', strtotime($year . '-' . $monthDigit . '-01')) . ' ' . $year),
+ $urlTemplate,
+ $month,
+ $template
+ );
+ }
+ }
+
+ public function compilePaginatedEntriesByDate($template = null)
+ {
+ if (null === $template) {
+ $template = $this->options->getByDayTemplate();
+ if (empty($template)) {
+ throw new \DomainException('No template provided for listing entries by day');
+ }
+ }
+
+ $filenameTemplate = $this->options->getByDayFilenameTemplate();
+ $urlTemplate = $this->options->getByDayUrlTemplate();
+ $titleTemplate = $this->options->getByDayTitle();
+
+ $this->prepareEntries();
+
+ foreach ($this->byDay as $day => $list) {
+ // Get the year, month, and day digits
+ list($year, $month, $date) = explode('/', $day, 3);
+
+ $this->iterateAndRenderList(
+ $list,
+ $filenameTemplate,
+ array($day),
+ sprintf($titleTemplate, $date . ' ' . date('F', strtotime($year . '-' . $month . '-' . $date)) . ' ' . $year),
+ $urlTemplate,
+ $day,
+ $template
+ );
+ }
+ }
+
+ public function compilePaginatedEntriesByTag($template = null)
+ {
+ if (null === $template) {
+ $template = $this->options->getByTagTemplate();
+ if (empty($template)) {
+ throw new \DomainException('No template provided for listing entries by tag');
+ }
+ }
+
+ $filenameTemplate = $this->options->getByTagFilenameTemplate();
+ $urlTemplate = $this->options->getByTagUrlTemplate();
+ $titleTemplate = $this->options->getByTagTitle();
+
+ $this->prepareEntries();
+
+ foreach ($this->byTag as $tag => $list) {
+ $this->iterateAndRenderList(
+ $list,
+ $filenameTemplate,
+ array($tag),
+ sprintf($titleTemplate, $tag),
+ $urlTemplate,
+ $tag,
+ $template
+ );
+ }
+ }
+
+ public function compileTagFeeds($type)
+ {
+ $type = strtolower($type);
+ if (!in_array($type, array('atom', 'rss'))) {
+ throw new InvalidArgumentException('Feed type must be "atom" or "rss"');
+ }
+
+ $filenameTemplate = $this->options->getTagFeedFilenameTemplate();
+ $blogLinkTemplate = $this->options->getTagFeedBlogLinkTemplate();
+ $feedLinkTemplate = $this->options->getTagFeedFeedLinkTemplate();
+ $titleTemplate = $this->options->getTagFeedTitleTemplate();
+
+ $this->prepareEntries();
+
+ foreach ($this->byTag as $tag => $list) {
+ $title = sprintf($titleTemplate, $tag);
+ $filename = sprintf($filenameTemplate, $tag, $type);
+ $blogLink = sprintf($blogLinkTemplate, str_replace(' ', '+', $tag));
+ $feedLink = sprintf($feedLinkTemplate, str_replace(' ', '+', $tag), $type);
+
+ $this->iterateAndGenerateFeed(
+ $type,
+ $list,
+ $title,
+ $blogLink,
+ $feedLink,
+ $filename
+ );
+ }
+ }
+
+
+ /**
+ * Compile a tag cloud from the entries
+ *
+ * @todo Should this write the tag cloud markup to a file?
+ * @return TagCloud
+ */
+ public function compileTagCloud()
+ {
+ if ($this->tagCloud) {
+ return $this->tagCloud;
+ }
+
+ $tagUrlTemplate = $this->options->getTagCloudUrlTemplate();
+ $cloudOptions = $this->options->getTagCloudOptions();
+
+ $this->prepareEntries();
+
+ $tags = array();
+ foreach ($this->byTag as $tag => $list) {
+ $tags[$tag] = array(
+ 'title' => $tag,
+ 'weight' => count($list),
+ 'params' => array(
+ 'url' => sprintf($tagUrlTemplate, str_replace(' ', '+', $tag)),
+ ),
+ );
+ }
+ $options['tags'] = $tags;
+
+ $this->tagCloud = new TagCloud($options);
+ return $this->tagCloud;
+ }
+
+ /**
+ * Prepare the list of entries
+ *
+ * Loops through the filesystem tree, looking for PHP files
+ * that return EntryEntity objects. For each returned, adds it
+ * to:
+ *
+ * - $entries, which has all entries
+ * - $byYear, a hash of year/SortedEntries pairs
+ * - $byMonth, a hash of year-month/SortedEntries pairs
+ * - $byDay, a hash of year-month-day/SortedEntries pairs
+ * - $byTag, a hash of tag/SortedEntries pairs
+ * - $byAuthor, a hash of author/SortedEntries pairs
+ *
+ * @return void
+ */
+ protected function prepareEntries()
+ {
+ if ($this->entries) {
+ return;
+ }
+
+ $this->entries = new Compiler\SortedEntries();
+ $this->pagedEntries = new Compiler\SortedEntries();
+ $this->byYear = array();
+ $this->byMonth = array();
+ $this->byDay = array();
+ $this->byTag = array();
+ $this->byAuthor = array();
+ foreach ($this->files as $file) {
+ $entry = include $file->getRealPath();
+ if (!$entry instanceof EntryEntity) {
+ continue;
+ }
+
+ if ($entry->isDraft()) {
+ continue;
+ }
+
+ // First, set in entries
+ $timestamp = $entry->getCreated();
+ $this->entries->insert($entry, $timestamp);
+
+ // Second, test if it's public; if not, continue to the next
+ if (!$entry->isPublic()) {
+ continue;
+ }
+
+ // Third, add to a special "paginated entries" list
+ $this->pagedEntries->insert($entry, $timestamp);
+
+ // Then, set in appropriate year, month, and day slots
+ $date = new DateTime();
+ $date->setTimestamp($timestamp)
+ ->setTimezone(new DateTimezone($entry->getTimezone()));
+
+ $year = $date->format('Y');
+ $month = $date->format('Y/m');
+ $day = $date->format('Y/m/d');
+
+ if (!isset($this->byYear[$year])) {
+ $this->byYear[$year] = new Compiler\SortedEntries();
+ }
+ $this->byYear[$year]->insert($entry, $timestamp);
+
+ if (!isset($this->byMonth[$month])) {
+ $this->byMonth[$month] = new Compiler\SortedEntries();
+ }
+ $this->byMonth[$month]->insert($entry, $timestamp);
+
+ if (!isset($this->byDay[$day])) {
+ $this->byDay[$day] = new Compiler\SortedEntries();
+ }
+ $this->byDay[$day]->insert($entry, $timestamp);
+
+ // Next, set in appropriate tag lists
+ foreach ($entry->getTags() as $tag) {
+ if (!isset($this->byTag[$tag])) {
+ $this->byTag[$tag] = new Compiler\SortedEntries();
+ }
+ $this->byTag[$tag]->insert($entry, $timestamp);
+ }
+
+ // Finally, by author
+ $author = $entry->getAuthor();
+ if (!isset($this->byAuthor[$author])) {
+ $this->byAuthor[$author] = new Compiler\SortedEntries();
+ }
+ $this->byAuthor[$author]->insert($entry, $timestamp);
+ }
+
+ // Cast to array to ensure we can loop through it multiple
+ // times; fixes the issue that a Heap removes entries during iteration
+ $this->entries = iterator_to_array($this->entries);
+ $this->pagedEntries = iterator_to_array($this->pagedEntries);
+
+ foreach (array('byYear', 'byMonth', 'byDay', 'byTag', 'byAuthor') as $prop) {
+ foreach ($this->$prop as $index => $heap) {
+ // have to do this due to dynamic resolution order in PHP
+ $local =& $this->$prop;
+ $local[$index] = iterator_to_array($heap);
+ }
+ }
+ }
+
+ /**
+ * Retrieve configured paginator
+ *
+ * We need following configuration
+ * - How many entries to include per page
+ * - How many pages to show in the paginator
+ * - Template for view script
+ * - Partial for paginator control
+ *
+ * @param Iterator|array $it
+ * @return Paginator
+ * @throws DomainException
+ */
+ protected function getPaginator(array $list)
+ {
+ $paginator = new Paginator(new ArrayPaginator($list));
+ $paginator->setItemCountPerPage($this->options->getPaginatorItemCountPerPage());
+ $paginator->setPageRange($this->options->getPaginatorPageRange());
+ return $paginator;
+ }
+
+ /**
+ * Prepare the response strategy
+ *
+ * Injects a new callback that imports the provided filename, and writes
+ * the rendering results to that file.
+ *
+ * @param string $filename
+ * @return void
+ */
+ protected function prepareResponseStrategy($filename)
+ {
+ if ($this->responseStrategyPrepared) {
+ $this->filename->file = $filename;
+ return;
+ }
+ $this->filename = new stdClass;
+ $this->filename->file = $filename;
+ $filename = $this->filename;
+ $writer = $this->writer;
+
+ $this->view->addResponseStrategy(function ($e) use ($filename, $writer) {
+ $result = $e->getResult();
+ $file = $filename->file;
+ $dir = dirname($file);
+ if (!file_exists($dir) || !is_dir($dir)) {
+ mkdir($dir, 0777, true);
+ }
+ if (preg_match('/-p1.html$/', $file)) {
+ $file = preg_replace('/-p1(\.html)$/', '$1', $file);
+ }
+ $file = str_replace(' ', '+', $file);
+ $writer->write($file, $result);
+ });
+ $this->responseStrategyPrepared = true;
+ }
+
+ protected function iterateAndRenderList(
+ $list,
+ $filenameTemplate,
+ array $filenameSubs,
+ $title,
+ $urlTemplate,
+ $substitution,
+ $template
+ ) {
+ // Get a paginator for this day
+ $paginator = $this->getPaginator($list);
+
+ // Loop through pages
+ $pageCount = count($paginator);
+ for ($i = 1; $i <= $pageCount; $i++) {
+ $paginator->setCurrentPageNumber($i);
+
+ $substitutions = $filenameSubs;
+ $substitutions[] = $i;
+ $filename = vsprintf($filenameTemplate, $substitutions);
+
+ // Generate this page
+ $model = new ViewModel(array(
+ 'title' => $title,
+ 'entries' => $paginator,
+ 'paginator_url' => $urlTemplate,
+ 'substitution' => $substitution,
+ ));
+ $model->setTemplate($template);
+
+ $this->prepareResponseStrategy($filename);
+ $this->view->render($model);
+
+ // This hack ensures that the paginator is reset for each page
+ if ($i <= $pageCount) {
+ $paginator = $this->getPaginator($list);
+ }
+ }
+ }
+
+ protected function iterateAndGenerateFeed(
+ $type,
+ $list,
+ $title,
+ $blogLink,
+ $feedLinkTemplate,
+ $filenameTemplate
+ ) {
+ $blogLink = $this->options->getFeedHostname() . $blogLink;
+ $feedLinkTemplate = $this->options->getFeedHostname() . $feedLinkTemplate;
+ $linkTemplate = $this->options->getFeedHostname() . $this->options->getEntryLinkTemplate();
+
+ // Get a paginator
+ $paginator = $this->getPaginator($this->pagedEntries);
+ $paginator->setCurrentPageNumber(1);
+
+ $feed = new FeedWriter();
+ $feed->setTitle($title);
+ $feed->setLink($blogLink);
+ $feed->setFeedLink(sprintf($feedLinkTemplate, $type), $type);
+
+ if ('rss' == $type) {
+ $feed->setDescription($title);
+ }
+
+ $authorUri = $this->options->getFeedAuthorUri();
+ if (empty($authorUri)) {
+ $authorUri = $blogLink;
+ }
+ $author = array(
+ 'name' => $this->options->getFeedAuthorName(),
+ 'email' => $this->options->getFeedAuthorEmail(),
+ 'uri' => $authorUri,
+ );
+
+ $latest = false;
+ foreach ($paginator as $post) {
+ if (!$latest) {
+ $latest = $post;
+ }
+ $entry = $feed->createEntry();
+ $entry->setTitle($post->getTitle());
+ $entry->setLink(sprintf($linkTemplate, $post->getId()));
+
+ $entry->addAuthor($author);
+ $entry->setDateModified($post->getUpdated());
+ $entry->setDateCreated($post->getCreated());
+ $entry->setContent($post->getBody());
+
+ $feed->addEntry($entry);
+ }
+
+ // Set feed date
+ $feed->setDateModified($latest->getUpdated());
+
+ // Write feed to file
+ $this->writer->write(sprintf($filenameTemplate, $type), $feed->export($type));
+ }
+}
View
10 src/PhlyBlog/Compiler/FileWriter.php
@@ -0,0 +1,10 @@
+<?php
+namespace PhlyBlog\Compiler;
+
+class FileWriter implements WriterInterface
+{
+ public function write($filename, $data)
+ {
+ file_put_contents($filename, $data);
+ }
+}
View
72 src/PhlyBlog/Compiler/PhpFileFilter.php
@@ -0,0 +1,72 @@
+<?php
+namespace PhlyBlog\Compiler;
+
+use DirectoryIterator,
+ FilterIterator,
+ InvalidArgumentException,
+ Iterator,
+ RecursiveDirectoryIterator,
+ RecursiveIterator,
+ RecursiveIteratorIterator,
+ SplFileInfo;
+
+/**
+ * Usage:
+ *
+ * <code>
+ * $files = new PhpFileFilter($path);
+ *
+ * // or
+ * $dir = new DirectoryIterator($path);
+ * $files = new PhpFileIterator($dir);
+ *
+ * // or
+ * $dir = new RecursiveDirectoryIterator($path);
+ * $files = new PhpFileIterator($dir);
+ * </code>
+ */
+class PhpFileFilter extends FilterIterator
+{
+ public function __construct($dirOrIterator = '.')
+ {
+ if (is_string($dirOrIterator)) {
+ if (!is_dir($dirOrIterator)) {
+ throw new InvalidArgumentException('Expected a valid directory name');
+ }
+
+ $dirOrIterator = new RecursiveDirectoryIterator($dirOrIterator);
+ }
+ if (!$dirOrIterator instanceof DirectoryIterator) {
+ throw new InvalidArgumentException('Expected a DirectoryIterator');
+ }
+
+ if ($dirOrIterator instanceof RecursiveIterator) {
+ $iterator = new RecursiveIteratorIterator($dirOrIterator);
+ } else {
+ $iterator = $dirOrIterator;
+ }
+
+ parent::__construct($iterator);
+ $this->rewind();
+ }
+
+ public function accept()
+ {
+ $current = $this->getInnerIterator()->current();
+ if (!$current instanceof SplFileInfo) {
+ return false;
+ }
+
+ if (!$current->isFile()) {
+ return false;
+ }
+
+ $ext = $current->getExtension();
+ if ($ext != 'php') {
+ return false;
+ }
+
+ return true;
+ }
+}
+
View
39 src/PhlyBlog/Compiler/SortedEntries.php
@@ -0,0 +1,39 @@
+<?php
+namespace PhlyBlog\Compiler;
+
+use SplPriorityQueue,
+ PhlyBlog\EntryEntity;
+
+class SortedEntries extends SplPriorityQueue
+{
+ /**
+ * Sorting on timestamps
+ *
+ * @param int $priority1
+ * @param int $priority2
+ * @return int
+ */
+ public function compare($priority1, $priority2)
+ {
+ if ($priority1 > $priority2) {
+ return 1;
+ }
+ if ($priority1 < $priority2) {
+ return -1;
+ }
+ // equal
+ return 0;
+ }
+
+ public function insert($data, $priority)
+ {
+ if (!$data instanceof EntryEntity) {
+ throw new InvalidArgumentException(sprintf(
+ '%s expects an EntryEntity; received %s',
+ __METHOD__,
+ (is_object($data) ? get_class($data) : gettype($data))
+ ));
+ }
+ parent::insert($data, $priority);
+ }
+}
View
7 src/PhlyBlog/Compiler/WriterInterface.php
@@ -0,0 +1,7 @@
+<?php
+namespace PhlyBlog\Compiler;
+
+interface WriterInterface
+{
+ public function write($filename, $data);
+}
View
554 src/PhlyBlog/CompilerOptions.php
@@ -0,0 +1,554 @@
+<?php
+namespace PhlyBlog;
+
+use Zend\Stdlib\Options,
+ Zend\Uri\UriFactory;
+
+class CompilerOptions extends Options
+{
+ protected $entryTemplate;
+
+ public function setEntryTemplate($entryTemplate)
+ {
+ $this->entryTemplate = (string) $entryTemplate;
+ return $this;
+ }
+
+ public function getEntryTemplate()
+ {
+ return $this->entryTemplate;
+ }
+
+ protected $entryFilenameTemplate = '%s.html';
+
+ public function setEntryFilenameTemplate($entryFilenameTemplate)
+ {
+ $this->entryFilenameTemplate = (string) $entryFilenameTemplate;
+ return $this;
+ }
+
+ public function getEntryFilenameTemplate()
+ {
+ return $this->entryFilenameTemplate;
+ }
+
+
+ /* Used everywhere */
+ protected $entryLinkTemplate = '/blog/%s.html';
+
+ public function setEntryLinkTemplate($entryLinkTemplate)
+ {
+ $this->entryLinkTemplate = (string) $entryLinkTemplate;
+ return $this;
+ }
+
+ public function getEntryLinkTemplate()
+ {
+ return $this->entryLinkTemplate;
+ }
+
+ /* Used for feeds */
+ protected $feedHostname = 'http://localhost';
+
+ public function setFeedHostname($feedHostname)
+ {
+ $this->feedHostname = (string) $feedHostname;
+ return $this;
+ }
+
+ public function getFeedHostname()
+ {
+ return $this->feedHostname;
+ }
+
+
+ protected $entriesTemplate;
+
+ public function setEntriesTemplate($entriesTemplate)
+ {
+ $this->entriesTemplate = (string) $entriesTemplate;
+ return $this;
+ }
+
+ public function getEntriesTemplate()
+ {
+ return $this->entriesTemplate;
+ }
+
+ protected $entriesFilenameTemplate = 'blog-p%d.html';
+
+ public function setEntriesFilenameTemplate($entriesFilenameTemplate)
+ {
+ $this->entriesFilenameTemplate = (string) $entriesFilenameTemplate;
+ return $this;
+ }
+
+ public function getEntriesFilenameTemplate()
+ {
+ return $this->entriesFilenameTemplate;
+ }
+
+ protected $entriesUrlTemplate = '/blog-p%d.html';
+
+ public function setEntriesUrlTemplate($entriesUrlTemplate)
+ {
+ $this->entriesUrlTemplate = (string) $entriesUrlTemplate;
+ return $this;
+ }
+
+ public function getEntriesUrlTemplate()
+ {
+ return $this->entriesUrlTemplate;
+ }
+
+ protected $entriesTitle = 'Blog Entries';
+
+ public function setEntriesTitle($entriesTitle)
+ {
+ $this->entriesTitle = (string) $entriesTitle;
+ return $this;
+ }
+
+ public function getEntriesTitle()
+ {
+ return $this->entriesTitle;
+ }
+
+ protected $feedFilename = 'blog-%s.xml';
+
+ public function setFeedFilename($feedFilename)
+ {
+ $this->feedFilename = (string) $feedFilename;
+ return $this;
+ }
+
+ public function getFeedFilename()
+ {
+ return $this->feedFilename;
+ }
+
+ protected $feedBlogLink = '/blog.html';
+
+ public function setFeedBlogLink($feedBlogLink)
+ {
+ $this->feedBlogLink = (string) $feedBlogLink;
+ return $this;
+ }
+
+ public function getFeedBlogLink()
+ {
+ return $this->feedBlogLink;
+ }
+
+ protected $feedFeedLink = '/blog-%s.xml';
+
+ public function setFeedFeedLink($feedFeedLink)
+ {
+ $this->feedFeedLink = (string) $feedFeedLink;
+ return $this;
+ }
+
+ public function getFeedFeedLink()
+ {
+ return $this->feedFeedLink;
+ }
+
+ protected $feedTitle = 'Blog';
+
+ public function setFeedTitle($feedTitle)
+ {
+ $this->feedTitle = (string) $feedTitle;
+ return $this;
+ }
+
+ public function getFeedTitle()
+ {
+ return $this->feedTitle;
+ }
+
+
+ protected $byYearTemplate;
+
+ public function setByYearTemplate($byYearTemplate)
+ {
+ $this->byYearTemplate = (string) $byYearTemplate;
+ return $this;
+ }
+
+ public function getByYearTemplate()
+ {
+ $template = $this->byYearTemplate;
+ if (empty($template)) {
+ $template = $this->getEntriesTemplate();
+ }
+ return $template;
+ }
+
+ protected $byYearFilenameTemplate = 'year/%s-p%d.html';
+
+ public function setByYearFilenameTemplate($byYearFilenameTemplate)
+ {
+ $this->byYearFilenameTemplate = (string) $byYearFilenameTemplate;
+ return $this;
+ }
+
+ public function getByYearFilenameTemplate()
+ {
+ return $this->byYearFilenameTemplate;
+ }
+
+ protected $byYearUrlTemplate = '/blog/year/%s-p%d.html';
+
+ public function setByYearUrlTemplate($byYearUrlTemplate)
+ {
+ $this->byYearUrlTemplate = (string) $byYearUrlTemplate;
+ return $this;
+ }
+
+ public function getByYearUrlTemplate()
+ {
+ return $this->byYearUrlTemplate;
+ }
+
+ protected $byYearTitle = 'Blog Entries for %d';
+
+ public function setByYearTitle($byYearTitle)
+ {
+ $this->byYearTitle = (string) $byYearTitle;
+ return $this;
+ }
+
+ public function getByYearTitle()
+ {
+ return $this->byYearTitle;
+ }
+
+ protected $byMonthTemplate;
+
+ public function setByMonthTemplate($byMonthTemplate)
+ {
+ $this->byMonthTemplate = (string) $byMonthTemplate;
+ return $this;
+ }
+
+ public function getByMonthTemplate()
+ {
+ $template = $this->byMonthTemplate;
+ if (empty($template)) {
+ $template = $this->getEntriesTemplate();
+ }
+ return $template;
+ }
+
+ protected $byMonthFilenameTemplate = 'month/%s-p%d.html';
+
+ public function setByMonthFilenameTemplate($byMonthFilenameTemplate)
+ {
+ $this->byMonthFilenameTemplate = (string) $byMonthFilenameTemplate;
+ return $this;
+ }
+
+ public function getByMonthFilenameTemplate()
+ {
+ return $this->byMonthFilenameTemplate;
+ }
+
+ protected $byMonthUrlTemplate = '/blog/month/%s-p%d.html';
+
+ public function setByMonthUrlTemplate($byMonthUrlTemplate)
+ {
+ $this->byMonthUrlTemplate = (string) $byMonthUrlTemplate;
+ return $this;
+ }
+
+ public function getByMonthUrlTemplate()
+ {
+ return $this->byMonthUrlTemplate;
+ }
+
+ protected $byMonthTitle = 'Blog Entries for %s';
+
+ public function setByMonthTitle($byMonthTitle)
+ {
+ $this->byMonthTitle = (string) $byMonthTitle;
+ return $this;
+ }
+
+ public function getByMonthTitle()
+ {
+ return $this->byMonthTitle;
+ }
+
+
+ protected $byDayTemplate;
+
+ public function setByDayTemplate($byDayTemplate)
+ {
+ $this->byDayTemplate = (string) $byDayTemplate;
+ return $this;
+ }
+
+ public function getByDayTemplate()
+ {
+ $template = $this->byDayTemplate;
+ if (empty($template)) {
+ $template = $this->getEntriesTemplate();
+ }
+ return $template;
+ }
+
+ protected $byDayFilenameTemplate = 'day/%s-p%d.html';
+
+ public function setByDayFilenameTemplate($byDayFilenameTemplate)
+ {
+ $this->byDayFilenameTemplate = (string) $byDayFilenameTemplate;
+ return $this;
+ }
+
+ public function getByDayFilenameTemplate()
+ {
+ return $this->byDayFilenameTemplate;
+ }
+
+ protected $byDayUrlTemplate = '/blog/day/%s-p%d.html';
+
+ public function setByDayUrlTemplate($byDayUrlTemplate)
+ {
+ $this->byDayUrlTemplate = (string) $byDayUrlTemplate;
+ return $this;
+ }
+
+ public function getByDayUrlTemplate()
+ {
+ return $this->byDayUrlTemplate;
+ }
+
+ protected $byDayTitle = 'Blog Entries for %s';
+
+ public function setByDayTitle($byDayTitle)
+ {
+ $this->byDayTitle = (string) $byDayTitle;
+ return $this;
+ }
+
+ public function getByDayTitle()
+ {
+ return $this->byDayTitle;
+ }
+
+
+ protected $byTagTemplate;
+
+ public function setByTagTemplate($byTagTemplate)
+ {
+ $this->byTagTemplate = (string) $byTagTemplate;
+ return $this;
+ }
+
+ public function getByTagTemplate()
+ {
+ $template = $this->byTagTemplate;
+ if (empty($template)) {
+ $template = $this->getEntriesTemplate();
+ }
+ return $template;
+ }
+
+ protected $byTagFilenameTemplate = 'tag/%s-p%d.html';
+
+ public function setByTagFilenameTemplate($byTagFilenameTemplate)
+ {
+ $this->byTagFilenameTemplate = (string) $byTagFilenameTemplate;
+ return $this;
+ }
+
+ public function getByTagFilenameTemplate()
+ {
+ return $this->byTagFilenameTemplate;
+ }
+
+ protected $byTagUrlTemplate = '/blog/tag/%s-p%d.html';
+
+ public function setByTagUrlTemplate($byTagUrlTemplate)
+ {
+ $this->byTagUrlTemplate = (string) $byTagUrlTemplate;
+ return $this;
+ }
+
+ public function getByTagUrlTemplate()
+ {
+ return $this->byTagUrlTemplate;
+ }
+
+ protected $byTagTitle = 'Tag: %s';
+
+ public function setByTagTitle($byTagTitle)
+ {
+ $this->byTagTitle = (string) $byTagTitle;
+ return $this;
+ }
+
+ public function getByTagTitle()
+ {
+ return $this->byTagTitle;
+ }
+
+
+ protected $tagFeedFilenameTemplate = 'blog/tag/%s-%s.xml';
+
+ public function setTagFeedFilenameTemplate($tagFeedFilenameTemplate)
+ {
+ $this->tagFeedFilenameTemplate = (string) $tagFeedFilenameTemplate;
+ return $this;
+ }
+
+ public function getTagFeedFilenameTemplate()
+ {
+ return $this->tagFeedFilenameTemplate;
+ }
+
+ protected $tagFeedBlogLinkTemplate = '/blog/tag/%s.html';
+
+ public function setTagFeedBlogLinkTemplate($tagFeedBlogLinkTemplate)
+ {
+ $this->tagFeedBlogLinkTemplate = (string) $tagFeedBlogLinkTemplate;
+ return $this;
+ }
+
+ public function getTagFeedBlogLinkTemplate()
+ {
+ return $this->tagFeedBlogLinkTemplate;
+ }
+
+ protected $tagFeedFeedLinkTemplate = '/blog/tag/%s-%s.xml';
+
+ public function setTagFeedFeedLinkTemplate($tagFeedFeedLinkTemplate)
+ {
+ $this->tagFeedFeedLinkTemplate = (string) $tagFeedFeedLinkTemplate;
+ return $this;
+ }
+
+ public function getTagFeedFeedLinkTemplate()
+ {
+ return $this->tagFeedFeedLinkTemplate;
+ }
+
+ protected $tagFeedTitleTemplate = 'Tag: %s';
+
+ public function setTagFeedTitleTemplate($tagFeedTitleTemplate)
+ {
+ $this->tagFeedTitleTemplate = (string) $tagFeedTitleTemplate;
+ return $this;
+ }
+
+ public function getTagFeedTitleTemplate()
+ {
+ return $this->tagFeedTitleTemplate;
+ }
+
+
+ protected $tagCloudUrlTemplate = '/blog/tag/%s.html';
+
+ public function setTagCloudUrlTemplate($tagCloudUrlTemplate)
+ {
+ $this->tagCloudUrlTemplate = (string) $tagCloudUrlTemplate;
+ return $this;
+ }
+
+ public function getTagCloudUrlTemplate()
+ {
+ return $this->tagCloudUrlTemplate;
+ }
+
+ protected $tagCloudOptions = array();
+
+ public function setTagCloudOptions(array $tagCloudOptions)
+ {
+ $this->tagCloudOptions = $tagCloudOptions;
+ return $this;
+ }
+
+ public function getTagCloudOptions()
+ {
+ return $this->tagCloudOptions;
+ }
+
+
+ protected $paginatorItemCountPerPage = 10;
+
+ public function setPaginatorItemCountPerPage($paginatorItemCountPerPage)
+ {
+ $paginatorItemCountPerPage = (int) $paginatorItemCountPerPage;
+ if ($paginatorItemCountPerPage < 1) {
+ throw new \InvalidArgumentException('Paginator item count per page must be at least 1');
+ }
+ $this->paginatorItemCountPerPage = (int) $paginatorItemCountPerPage;
+ return $this;
+ }
+
+ public function getPaginatorItemCountPerPage()
+ {
+ return $this->paginatorItemCountPerPage;
+ }
+
+ protected $paginatorPageRange = 10;
+
+ public function setPaginatorPageRange($paginatorPageRange)
+ {
+ $paginatorPageRange = (int) $paginatorPageRange;
+ if ($paginatorPageRange < 2) {
+ throw new \InvalidArgumentException('Paginator page range must be >= 2');
+ }
+ $this->paginatorPageRange = (int) $paginatorPageRange;
+ return $this;
+ }
+
+ public function getPaginatorPageRange()
+ {
+ return $this->paginatorPageRange;
+ }
+
+
+ protected $feedAuthorName = '';
+
+ public function setFeedAuthorName($feedAuthorName)
+ {
+ $this->feedAuthorName = (string) $feedAuthorName;
+ return $this;
+ }
+
+ public function getFeedAuthorName()
+ {
+ return $this->feedAuthorName;
+ }
+
+ protected $feedAuthorEmail = '';
+
+ public function setFeedAuthorEmail($feedAuthorEmail)
+ {
+ $this->feedAuthorEmail = (string) $feedAuthorEmail;
+ return $this;
+ }
+
+ public function getFeedAuthorEmail()
+ {
+ return $this->feedAuthorEmail;
+ }
+
+ protected $feedAuthorUri = null;
+
+ public function setFeedAuthorUri($feedAuthorUri)
+ {
+ $uri = UriFactory::factory($feedAuthorUri);
+ if (!$uri->isValid()) {
+ throw new \InvalidArgumentException('Author URI for feed is invalid');
+ }
+ $this->feedAuthorUri = $feedAuthorUri;
+ return $this;
+ }
+
+ public function getFeedAuthorUri()
+ {
+ return $this->feedAuthorUri;
+ }
+}
View
608 src/PhlyBlog/EntryEntity.php
@@ -0,0 +1,608 @@
+<?php
+namespace PhlyBlog;
+
+use CommonResource\Entity as EntityDefinition,
+ CommonResource\Filter\Timestamp as TimestampFilter,
+ Zend\Filter\InputFilter;
+
+class EntryEntity implements EntityDefinition
+{
+ protected static $defaultFilter;
+ protected $filter;
+
+ /*
+ * identifier/stub
+ * title
+ * body
+ * extended
+ * author
+ * is_draft
+ * is_public
+ * created
+ * updated
+ * tags (array)
+ * metadata (array)
+ */
+ protected $id;
+ protected $title;
+ protected $body = '';
+ protected $extended = '';
+ protected $author;
+ protected $isDraft = true;
+ protected $isPublic = true;
+ protected $created;
+ protected $updated;
+ protected $timezone = 'America/New_York';
+ protected $tags = array();
+ protected $metadata = array();
+ protected $comments = array();
+ protected $version = 2;
+
+ public static function makeStub($value)
+ {
+ $filter = new Filter\Permalink();
+ return $filter->filter($value);
+ }
+
+ /**
+ * Overloading: set property
+ *
+ * Proxies to setters
+ *
+ * @param string $name
+ * @param mixed $value
+ * @return void
+ * @throws UnexpectedValueException
+ */
+ public function __set($name, $value)
+ {
+ $method = 'set' . ucfirst($name);
+ if (method_exists($this, $method)) {
+ $this->$method($value);
+ return;
+ }
+ throw new \UnexpectedValueException(sprintf(
+ 'The property "%s" does not exist and cannot be set',
+ $name
+ ));
+ }
+
+ /**
+ * Overloading: retrieve property
+ *
+ * Proxies to getters
+ *
+ * @param string $name
+ * @return mixed
+ * @throws UnexpectedValueException
+ */
+ public function __get($name)
+ {
+ // Booleans:
+ if ('is' == substr($name, 0, 2)) {
+ if (method_exists($this, $name)) {
+ return $this->$name();
+ }
+ }
+
+ // Check for a getter
+ $method = 'get' . ucfirst($name);
+ if (method_exists($this, $method)) {
+ return $this->$method();
+ }
+
+ // Unknown
+ throw new \UnexpectedValueException(sprintf(
+ 'The property "%s" does not exist and cannot be retrieved',
+ $name
+ ));
+ }
+
+ /**
+ * Overloading: property exists
+ *
+ * @param string $name
+ * @return bool
+ */
+ public function __isset($name)
+ {
+ return property_exists($this, $name);
+ }
+
+ /**
+ *
+ * set value for identifier
+ * @param string $value
+ * @return Entry
+ */
+ public function setId($value)
+ {
+ $this->id = $value;
+ return $this;
+ }
+
+ /**
+ * Get value for identifier
+ *
+ * @return string
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * Set value for title
+ *
+ * @param string $value
+ * @return Entry
+ */
+ public function setTitle($value)
+ {
+ $this->title = $value;
+ if (empty($this->id)) {
+ $this->setId(static::makeStub($value));
+ }
+ return $this;
+ }
+
+ /**
+ * Get value for title
+ *
+ * @return string
+ */
+ public function getTitle()
+ {
+ return $this->title;
+ }
+
+ /**
+ * Set value for body
+ *
+ * @param string $value
+ * @return Entry
+ */
+ public function setBody($value)
+ {
+ $this->body = $value;
+ return $this;
+ }
+
+ /**
+ * Get value for body
+ *
+ * @return string
+ */
+ public function getBody()
+ {
+ return $this->body;
+ }
+
+ /**
+ * Set value for extended body
+ *
+ * @param string $value
+ * @return Entry
+ */
+ public function setExtended($value)
+ {
+ $this->extended = $value;
+ return $this;
+ }
+
+ /**
+ * Get value for extended body
+ *
+ * @return string
+ */
+ public function getExtended()
+ {
+ return $this->extended;
+ }
+
+ /**
+ * Set value for author
+ *
+ * @param string|object|array $value
+ * @return Entry
+ */
+ public function setAuthor($value)
+ {
+ $this->author = $value;
+ return $this;
+ }
+
+ /**
+ * Get value for author
+ *
+ * @return string|object|array
+ */
+ public function getAuthor()
+ {
+ return $this->author;
+ }
+
+ /**
+ * Set timestamp when entry was created
+ *
+ * @param DateTime|MongoDate|string|int $value
+ * @return Entry
+ */
+ public function setCreated($value)
+ {
+ $filter = new TimestampFilter;
+ $this->created = $filter->filter($value);
+ return $this;
+ }
+
+ /**
+ * Get value for created
+ *
+ * @return int
+ */
+ public function getCreated()
+ {
+ if (null === $this->created) {
+ $this->setCreated($_SERVER['REQUEST_TIME']);
+ }
+ return $this->created;
+ }
+
+ /**
+ * set value when entry updated
+ *
+ * @param int|string|MongoDate|DateTime $value
+ * @return Entry
+ */
+ public function setUpdated($value)
+ {
+ $filter = new TimestampFilter;
+ $this->updated = $filter->filter($value);
+ return $this;
+ }
+
+ /**
+ * Get value when entry updated
+ *
+ * @return int
+ */
+ public function getUpdated()
+ {
+ if (null === $this->updated) {
+ $this->setUpdated($_SERVER['REQUEST_TIME']);
+ }