diff --git a/.gitignore b/.gitignore index e27dc15..d6ab546 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ vendor composer.phar composer.lock -.DS_Store \ No newline at end of file +.DS_Store +*.sublime-* \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..12ed64a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +language: php + +branches: + only: + - master + +php: + - 5.3 + - 5.4 + +before_script: + - curl -s http://getcomposer.org/installer | php + - php composer.phar install --dev + +script: phpunit \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b962ff0 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Feather + +Feather is a lightweight, fully awesome forum built on the Laravel framework. + +[![Build Status](https://secure.travis-ci.org/featherforums/feather.png?branch=master)](https://travis-ci.org/featherforums/feather) \ No newline at end of file diff --git a/app/config/app.php b/app/config/app.php index b9ab5e2..fdc52b6 100644 --- a/app/config/app.php +++ b/app/config/app.php @@ -39,13 +39,20 @@ | */ - 'locales' => array('en'), - 'locale' => 'en', - 'fallback_locale' => 'en', + /* + |-------------------------------------------------------------------------- + | Application Fallback Locale + |-------------------------------------------------------------------------- + | + | The fallback locale determines the locale to use when the current one + | is not available. You may change the value to correspond to any of + | the language folders that are provided through your application. + | + */ - 'locale_path' => __DIR__.'/../lang', + 'fallback_locale' => 'en', /* |-------------------------------------------------------------------------- @@ -62,58 +69,89 @@ /* |-------------------------------------------------------------------------- - | Class Aliases + | Autoloaded Service Providers |-------------------------------------------------------------------------- | - | This array of class aliases will be registered when this application - | is started. However, feel free to register as many as you wish as - | the aliases are "lazy" loaded so they don't hinder performance. + | The service providers listed here will be automatically loaded on the + | request to your application. Feel free to add your own services to + | this array to grant expanded functionality to your applications. | */ - 'aliases' => array( + 'providers' => array( - 'Controller' => 'Illuminate\Routing\Controllers\Controller', - 'Eloquent' => 'Illuminate\Database\Eloquent\Model', + 'Illuminate\Foundation\Providers\AliasServiceProvider', + 'Illuminate\Foundation\Providers\ArtisanServiceProvider', + 'Illuminate\Auth\AuthServiceProvider', + 'Illuminate\Cache\CacheServiceProvider', + 'Illuminate\Foundation\Providers\ComposerServiceProvider', + 'Illuminate\Routing\ControllerServiceProvider', + 'Illuminate\CookieServiceProvider', + 'Illuminate\Database\DatabaseServiceProvider', + 'Illuminate\EncryptionServiceProvider', + 'Illuminate\Events\EventServiceProvider', + 'Illuminate\FilesystemServiceProvider', + 'Illuminate\Hashing\HashServiceProvider', + 'Illuminate\Log\LogServiceProvider', + 'Illuminate\Mail\MailServiceProvider', + 'Illuminate\Database\MigrationServiceProvider', + 'Illuminate\Pagination\PaginationServiceProvider', + 'Illuminate\Foundation\Providers\PublisherServiceProvider', + 'Illuminate\Redis\RedisServiceProvider', + 'Illuminate\Database\SeedServiceProvider', + 'Illuminate\Session\SessionServiceProvider', + 'Illuminate\Translation\TranslationServiceProvider', + 'Illuminate\Validation\ValidationServiceProvider', + 'Illuminate\View\ViewServiceProvider', + 'Basset\BassetServiceProvider', ), /* |-------------------------------------------------------------------------- - | Autoloaded Service Providers + | Class Aliases |-------------------------------------------------------------------------- | - | The service providers listed here will be automatically loaded on the - | request to your application. Feel free to add your own services to - | this array to grant expanded functionality to your applications. + | This array of class aliases will be registered when this application + | is started. However, feel free to register as many as you wish as + | the aliases are "lazy" loaded so they don't hinder performance. | */ - 'providers' => array( + 'aliases' => array( - 'Illuminate\Foundation\Providers\AliasServiceProvider', - 'Illuminate\Foundation\Providers\ArtisanServiceProvider', - 'Illuminate\Foundation\Providers\AuthServiceProvider', - 'Illuminate\Foundation\Providers\CacheServiceProvider', - 'Illuminate\Foundation\Providers\ComposerServiceProvider', - 'Illuminate\Foundation\Providers\ControllerServiceProvider', - 'Illuminate\Foundation\Providers\CookieServiceProvider', - 'Illuminate\Foundation\Providers\DatabaseServiceProvider', - 'Illuminate\Foundation\Providers\EncrypterServiceProvider', - 'Illuminate\Foundation\Providers\EventsServiceProvider', - 'Illuminate\Foundation\Providers\FilesystemServiceProvider', - 'Illuminate\Foundation\Providers\HashServiceProvider', - 'Illuminate\Foundation\Providers\LogServiceProvider', - 'Illuminate\Foundation\Providers\MailServiceProvider', - 'Illuminate\Foundation\Providers\MigrationServiceProvider', - 'Illuminate\Foundation\Providers\PaginationServiceProvider', - 'Illuminate\Foundation\Providers\PublisherServiceProvider', - 'Illuminate\Foundation\Providers\RedisServiceProvider', - 'Illuminate\Foundation\Providers\SeedServiceProvider', - 'Illuminate\Foundation\Providers\SessionServiceProvider', - 'Illuminate\Foundation\Providers\TranslationServiceProvider', - 'Illuminate\Foundation\Providers\ValidatorServiceProvider', - 'Illuminate\Foundation\Providers\ViewServiceProvider', + 'App' => 'Illuminate\Foundation\Facades\App', + 'Artisan' => 'Illuminate\Foundation\Facades\Artisan', + 'Auth' => 'Illuminate\Foundation\Facades\Auth', + 'Basset' => 'Basset\Facades\Basset', + 'Cache' => 'Illuminate\Foundation\Facades\Cache', + 'Config' => 'Illuminate\Foundation\Facades\Config', + 'Controller' => 'Illuminate\Routing\Controllers\Controller', + 'Cookie' => 'Illuminate\Foundation\Facades\Cookie', + 'Crypt' => 'Illuminate\Foundation\Facades\Crypt', + 'Crypt' => 'Illuminate\Foundation\Facades\Crypt', + 'DB' => 'Illuminate\Foundation\Facades\DB', + 'Eloquent' => 'Illuminate\Database\Eloquent\Model', + 'Event' => 'Illuminate\Foundation\Facades\Event', + 'Feather\Extension' => 'Feather\Facades\Extension', + 'Feather\Presenter' => 'Feather\Facades\Presenter', + 'File' => 'Illuminate\Foundation\Facades\File', + 'Hash' => 'Illuminate\Foundation\Facades\Hash', + 'Input' => 'Illuminate\Foundation\Facades\Input', + 'Lang' => 'Illuminate\Foundation\Facades\Lang', + 'Log' => 'Illuminate\Foundation\Facades\Log', + 'Mail' => 'Illuminate\Foundation\Facades\Mail', + 'Paginator' => 'Illuminate\Foundation\Facades\Paginator', + 'Redirect' => 'Illuminate\Foundation\Facades\Redirect', + 'Redis' => 'Illuminate\Foundation\Facades\Redis', + 'Request' => 'Illuminate\Foundation\Facades\Request', + 'Response' => 'Illuminate\Foundation\Facades\Response', + 'Route' => 'Illuminate\Foundation\Facades\Route', + 'Schema' => 'Illuminate\Foundation\Facades\Schema', + 'Session' => 'Illuminate\Foundation\Facades\Session', + 'URL' => 'Illuminate\Foundation\Facades\URL', + 'Validator' => 'Illuminate\Foundation\Facades\Validator', + 'View' => 'Illuminate\Foundation\Facades\View', ), diff --git a/app/config/database.php b/app/config/database.php index e8d6ce8..4b93333 100644 --- a/app/config/database.php +++ b/app/config/database.php @@ -55,11 +55,11 @@ 'mysql' => array( 'driver' => 'mysql', 'host' => 'localhost', - 'database' => 'database', + 'database' => 'feather', 'username' => 'root', 'password' => '', 'charset' => 'utf8', - 'collation' => 'utf8_unicode_ci', + 'collation' => 'utf8_general_ci', 'prefix' => '', ), diff --git a/public/packages/.gitkeep b/app/config/packages/.gitkeep similarity index 100% rename from public/packages/.gitkeep rename to app/config/packages/.gitkeep diff --git a/app/config/packages/jasonlewis/basset.php b/app/config/packages/jasonlewis/basset.php new file mode 100644 index 0000000..ef90021 --- /dev/null +++ b/app/config/packages/jasonlewis/basset.php @@ -0,0 +1,122 @@ + 'assets', + + /* + |-------------------------------------------------------------------------- + | Asset Directories + |-------------------------------------------------------------------------- + | + | These named directories are used for quick reference as well as when + | searching for an asset. Assets are located by cascading through the array + | of directories until an asset with the matching name is found. + | + | Directories are relative from the root of your application. + | + | You can specifiy an absolute path to a directory by prefixing it with + | 'path: '. + | + | array( + | 'css' => 'path: /path/to/your/directory' + | ) + | + */ + + 'directories' => array(), + + /* + |-------------------------------------------------------------------------- + | Asset Collections + |-------------------------------------------------------------------------- + | + | Define your collections in an array like so. + | + | array( + | 'website' => function($collection) + | { + | $collection->add('example.css'); + | } + | ) + | + | This collection is now available at Basset::show('website.css') + | + */ + 'collections' => array(), + + /* + |-------------------------------------------------------------------------- + | Compiling Path + |-------------------------------------------------------------------------- + | + | When assets are statically compiled via the command line the generated + | files will be stored in this directory. The path is relative to the root + | of your application, so remember to compile them to the public directory + | somewhere. + | + | If the directory does not exist, Basset will attempt to create it. + | + */ + + 'compiling_path' => 'public/assets', + + /* + |-------------------------------------------------------------------------- + | Production Environment + |-------------------------------------------------------------------------- + | + | Basset will attempt to detect your production environment and serve + | static assets. You can help Basset out in a number of ways to speed it up + | a bit. + | + | Set your actual production environment here and Basset will compare + | environments and serve the appropriate assets. + | + | Set to null or an empty string and Basset will try and detect your + | environment, this may deliver unexpected results. + | + | Set to false and Basset will always serve individual assets as it does in + | a development environment. Remember that filters will not be applied + | to the assets. + | + | Set to true to always serve static assets if available. + | + */ + + 'production_environment' => '', + + /* + |-------------------------------------------------------------------------- + | Named Filters + |-------------------------------------------------------------------------- + | + | A named filter can be used to quickly apply a filter to a collection of + | assets. + | + | 'YuiCss' => 'Yui\CssCompressorFilter' + | + | If you'd like to specify options for a named filter you can define the + | filter as an array. + | + | 'YuiCss' => array( + | 'Yui\CssCompressorFilter' => array('/path/to/yuicompressor.jar') + | ) + | + | The filter can then be referenced by its name when applying filters. + | + */ + + 'filters' => array() + +); \ No newline at end of file diff --git a/app/config/testing/cache.php b/app/config/testing/cache.php new file mode 100644 index 0000000..16d3ae2 --- /dev/null +++ b/app/config/testing/cache.php @@ -0,0 +1,20 @@ + 'array', + +); \ No newline at end of file diff --git a/app/config/testing/session.php b/app/config/testing/session.php new file mode 100644 index 0000000..338aeba --- /dev/null +++ b/app/config/testing/session.php @@ -0,0 +1,21 @@ + 'array', + +); \ No newline at end of file diff --git a/app/config/view.php b/app/config/view.php index 0b47626..eba10a4 100644 --- a/app/config/view.php +++ b/app/config/view.php @@ -2,21 +2,6 @@ return array( - /* - |-------------------------------------------------------------------------- - | Default View Driver - |-------------------------------------------------------------------------- - | - | This option controls the default view driver that will get used for the - | application. Laravel comes with a few great engines ranging from the - | simple PHP engine to the powerful Blade engine. Use what you like! - | - | Supported: "php", "blade" - | - */ - - 'driver' => 'blade', - /* |-------------------------------------------------------------------------- | View Storage Paths @@ -28,24 +13,7 @@ | */ - 'paths' => array( - - __DIR__.'/../views', - - ), - - /* - |-------------------------------------------------------------------------- - | Compiled View Cache Path - |-------------------------------------------------------------------------- - | - | Some view engines cache a compiled version of their templates, such as - | Twig and Blade. Here you may control where those caches are stored. - | A sensible default has already been setup for your applications. - | - */ - - 'cache' => __DIR__.'/../storage/views', + 'paths' => array(__DIR__.'/../views'), /* |-------------------------------------------------------------------------- diff --git a/app/controllers/BaseController.php b/app/controllers/BaseController.php new file mode 100644 index 0000000..817e208 --- /dev/null +++ b/app/controllers/BaseController.php @@ -0,0 +1,56 @@ +app = Facade::getFacadeApplication(); + + if ($this->layout) + { + $this->layout = $this->app['view']->make($this->layout); + } + } + + /** + * Call the given action with the given parameters. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + protected function directCallAction($method, $parameters) + { + // If no response is returned by the controller method then we'll fall back to + // the layout that was bound in the constructor. + if ( ! $response = call_user_func_array(array($this, $method), $parameters)) + { + $response = $this->layout; + } + + return $response; + } + +} \ No newline at end of file diff --git a/app/controllers/HomeController.php b/app/controllers/HomeController.php index 7eb9874..8ff715b 100644 --- a/app/controllers/HomeController.php +++ b/app/controllers/HomeController.php @@ -1,23 +1,12 @@ - array('place', 'place_id'), + 'user' => array('user', 'user_id'), + 'author' => array('user', 'user_id') + ); + + /** + * Get a new query builder for the model's table. + * + * @return Feather\Models\Builder + */ + public function newQuery() + { + $builder = new EloquentBuilder($this->newBaseQueryBuilder()); + + // Once we have the query builders, we will set the model instances so the + // builder can easily access any information it may need from the model + // while it is constructing and executing various queries against it. + $builder->setModel($this); + + return $builder; + } + + /** + * Handle the dynamic retrieval of attributes and associations. + * + * @param string $key + * @return mixed + */ + public function __get($key) + { + if (isset($this->cachable[$key]) and ! isset($this->attributes[$key]) and ! isset($this->relations[$key])) + { + list($group, $foreign) = $this->cachable[$key]; + + if (Cache::has("{$group}_{$this->$foreign}")) + { + return Cache::get("{$group}_{$this->$foreign}"); + } + } + + return parent::__get($key); + } + +} \ No newline at end of file diff --git a/app/models/Config.php b/app/models/Config.php new file mode 100644 index 0000000..18b149e --- /dev/null +++ b/app/models/Config.php @@ -0,0 +1,34 @@ +get(); + }); + } + +} \ No newline at end of file diff --git a/app/models/Migrator.php b/app/models/Migrator.php new file mode 100644 index 0000000..2e9acb7 --- /dev/null +++ b/app/models/Migrator.php @@ -0,0 +1,35 @@ +where('driver', $driver)->first(); + } + + public function getOptions($options) + { + return (object) json_decode($options); + } + +} \ No newline at end of file diff --git a/app/models/User.php b/app/models/User.php index e2bcec5..3caa0ae 100644 --- a/app/models/User.php +++ b/app/models/User.php @@ -1,27 +1,29 @@ -password; } + /** + * A user can have many and belong to many roles. + * + * @return object + */ + public function roles() + { + return $this->belongsToMany('Feather\\Core\\Role', 'user_roles', 'user_id', 'role_id'); + } + + /** + * When setting a password it must be hashed before stored in the database. + * + * @param string $password + * @return void + */ + public function setPassword($password) + { + $this->setAttribute('password', Hash::make($password)); + } + + /** + * Getter for a users slug. + * + * @return string + */ + public function getSlug() + { + return Str::slug($this->getAttribute('username')); + } + + /** + * Getter for a users name. + * + * @return string + */ + public function getName() + { + return $this->getAttribute('username'); + } + + /** + * Getter for a users avatar URL, provided by Gravatar. + * + * @return string + */ + public function getAvatar() + { + return 'http://www.gravatar.com/avatar/' . md5(strtolower(trim($this->getAttribute('email')))); + } + + /** + * Register a new user and return the new user object. + * + * @param array $input + * @param bool $activated + * @return Feather\Core\User + */ + public function register($input) + { + $this->fill(array( + 'username' => $input['username'], + 'password' => $input['password'], + 'email' => $input['email'] + )); + + if(Config::get('feather: db.registration.confirm_email')) + { + $this->fill(array( + 'activation_key' => Str::random(30), + 'activated' => 0 + )); + } + + if(!$this->save()) + { + throw new FeatherModelException; + } + + // Attach the users role. If the user needs to confirm their e-mail address they are given + // the confirming role status which has an ID of 4. If not then they receive the standard + // member role which has an ID of 2. + $this->roles()->attach(Config::get('feather: db.registration.confirm_email') ? 4 : 2); + + return $this; + } + + public static function edit($user, $input) + { + + } + + /** + * Register a new user and associate it with an e-mail from an authenticator. + * + * @param array $associate + * @param array $input + * @return Feather\Core\User + */ + public static function associate($associate, $input) + { + $user = new static(array( + 'username' => $input['username'], + 'authenticator' => Config::get('feather: db.auth.driver'), + 'authenticator_token' => $associate['token'], + 'authenticator_associated_email' => $associate['email'], + 'email' => $input['email'] + )); + + if(!$user->save()) + { + throw new FeatherModelException; + } + + $user->roles()->attach(2); + + return $user; + } + } \ No newline at end of file diff --git a/app/models/User/Roles.php b/app/models/User/Roles.php new file mode 100644 index 0000000..e69de29 diff --git a/app/routes.php b/app/routes.php index e2a75e2..1e1cf7e 100644 --- a/app/routes.php +++ b/app/routes.php @@ -11,7 +11,4 @@ | */ -Route::get('/', function() -{ - return View::make('hello'); -}); \ No newline at end of file +Route::get('/', 'Feather\Controllers\HomeController@showHomepage'); \ No newline at end of file diff --git a/app/start/artisan.php b/app/start/artisan.php index 14de2e9..267f07f 100644 --- a/app/start/artisan.php +++ b/app/start/artisan.php @@ -1,4 +1,4 @@ -resolveCommands(array( + 'command.feather', + 'command.feather.publish', + 'command.feather.install' +)); \ No newline at end of file diff --git a/app/start/console.php b/app/start/console.php new file mode 100644 index 0000000..278d73d --- /dev/null +++ b/app/start/console.php @@ -0,0 +1,45 @@ +share(function($app) +{ + return new Console\FeatherCommand; +}); + +/* +|-------------------------------------------------------------------------- +| Publish Command +|-------------------------------------------------------------------------- +| +| The publish command is responsible for publishing assets in both themes +| and extensions. +| +*/ + +$app['command.feather.publish'] = $app->share(function($app) +{ + return new Console\PublishCommand($app['asset.publisher'], $app['path.themes'], $app['path.extensions']); +}); + +/* +|-------------------------------------------------------------------------- +| Install Command +|-------------------------------------------------------------------------- +| +| The install command is responsible for installing Feather in the current +| location. +| +*/ + +$app['command.feather.install'] = $app->share(function($app) +{ + return new Console\InstallCommand; +}); \ No newline at end of file diff --git a/app/start/facades.php b/app/start/facades.php new file mode 100644 index 0000000..09b5626 --- /dev/null +++ b/app/start/facades.php @@ -0,0 +1,25 @@ +runningInConsole()) +{ + if ($app['cache']->has('config')) + { + $app['cache']->forget('config'); + } + + $config = new Models\Config; + + foreach ($config->everything() as $item) + { + $app['config']->set("feather.{$item->name}", $item->value); + } +} /* |-------------------------------------------------------------------------- -| Application Error Handler +| Feather Providers |-------------------------------------------------------------------------- | -| Here you may handle any errors that occur in your application, including -| logging them or displaying custom views for specific errors. You may -| even register several error handlers to handle different types of -| exceptions. If nothing is returned, the default error view is -| shown, which includes a detailed stack trace during debug. +| Now that most of Feather has been bootstrapped we can register the other +| providers that Feather uses. This keeps everything neat and tidy. | */ -App::error(function(Exception $exception, $code) +$providers = array( + 'Feather\Presenter\PresenterServiceProvider', + 'Feather\Extensions\ExtensionServiceProvider' +); + +foreach ($providers as $provider) { - Log::error($exception); -}); + $app->register(new $provider($app)); +} + +/* +|-------------------------------------------------------------------------- +| Require The Facades File +|-------------------------------------------------------------------------- +| +| We'll now register some facades to easily access some of Feather's +| components statically. +| +*/ + +require_once __DIR__.'/facades.php'; + +/* +|-------------------------------------------------------------------------- +| Require The Console File +|-------------------------------------------------------------------------- +| +| Commands are registered with the application container and when Artisan +| is run they are resolved from the Artisan start file. +| +*/ + +require __DIR__.'/console.php'; + +/* +|-------------------------------------------------------------------------- +| Prepare Feather Presenter +|-------------------------------------------------------------------------- +| +| Presenter is responsible for Feather's views. We'll prepare the presenter +| now, which will start the theme and set some view paths. +| +*/ + +$app['feather.presenter']->prepare(); + +/* +|-------------------------------------------------------------------------- +| Feather Extensions +|-------------------------------------------------------------------------- +| +| Register the activated extensions with Feather. +| +*/ + +if ( ! $app->runningInConsole()) +{ + $extensions = new Models\Extension; + + $app['feather.extensions']->registerExtensions($extensions->getActivated()->toArray()); +} /* |-------------------------------------------------------------------------- diff --git a/app/start/local.php b/app/start/local.php deleted file mode 100644 index adab104..0000000 --- a/app/start/local.php +++ /dev/null @@ -1,3 +0,0 @@ -client->request('GET', '/'); - - $this->assertTrue($this->client->getResponse()->isOk()); - - $this->assertCount(1, $crawler->filter('h1:contains("Hello World!")')); - } - -} \ No newline at end of file diff --git a/app/themes/basic/public/css/theme.css b/app/themes/basic/public/css/theme.css new file mode 100644 index 0000000..f4daf7d --- /dev/null +++ b/app/themes/basic/public/css/theme.css @@ -0,0 +1,423 @@ +body { + background-image: url('../img/background.png'); + padding: 0; + margin: 0; + font-family: "Helvetica", "Helvetica Neue", Arial, sans-serif; + font-size: 13px; + color: #454545; + line-height: 1.45em; +} + +input, button, textarea { + color: #454545; + font-family: "Helvetica", "Helvetica Neue", Arial, sans-serif; + font-size: 1em; +} + +ul { + list-style-type: none; +} + +a { + color: #d7634c; +} + +a:hover { + text-decoration: none; +} + +.group:after, +fieldset dd:after, +.alert-floated:after { + content: ""; + display: table; + clear: both; +} + +/** + * The container element holds all other elements and is centered on our screen. + */ +.container { + position: relative; + width: 1140px; + margin: 0 auto; + padding: 0; +} + +/** + * The header contains the logo and some vital navigation for getting around the forum. + * Our user related data will also appear here when they are signed in. + */ +.header > .logo { + clear: right; + float: left; +} + +/** + * The user section of the header is above everything and is easily to see, it's fixed. + */ +.header > .user { + float: right; + margin: 10px 0 30px; + font-size: 14px; +} + +.header > .user a { + text-decoration: none; +} + +.header > .user > ul { + list-style-type: none; + margin: 0; + padding: 0; +} + +.header > .user > ul > li { + float: left; + padding: 0 18px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + background-color: #fff; + margin: 0 0 0 6px; + position: relative; + line-height: 34px; +} + +.header > .user > ul > li.light { background-color: #d1c9c1; } +.header > .user > ul > li.light:hover { background-color: #d9d2cb; } + +.header > .user > ul > li.dark { background: rgba(0, 0, 0, 0.6); } +.header > .user > ul > li.dark:hover { background: rgba(0, 0, 0, 0.7); } + +.header > .user > ul > li.attn { background: rgba(215, 99, 76, 0.8); } +.header > .user > ul > li.attn:hover { background: rgba(215, 99, 76, 0.95); } + +.header > .user > ul > li.link > a { + display: block; + margin: 0 -18px; + padding: 0 18px; +} + +.header > .user > ul > li.dark a, +.header > .user > ul > li.attn a { + color: #fff; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.15); +} + +/** + * When a menu with options is hovered we show another dropdown menu. + */ +.header > .user > ul > li.has-options:hover { + -moz-border-radius: 3px 3px 0 0; + -webkit-border-radius: 3px 3px 0 0; + border-radius: 3px 3px 0 0; +} + +.header > .user > ul > li.has-options > ul { + display: none; + list-style-type: none; + margin: 0; + padding: 0; + position: absolute; + top: 40px; + left: 0; + line-height: normal; + width: 350px; + -moz-border-radius: 0 2px 2px 2px; + -webkit-border-radius: 0 2px 2px 2px; + border-radius: 0 2px 2px 2px; + overflow: hidden; +} + +.header > .user > ul > li.dark.has-options > ul { background: rgba(0, 0, 0, 0.7); } +.header > .user > ul > li.attn.has-options > ul { background: rgba(215, 99, 76, 0.95); } + +.header > .user > ul > li.has-options:hover > ul { + display: block; +} + +/** + * Place a smaller bar after the hovered option so we can shift the actual dropdown below + * other menu items. + */ +.header > .user > ul > li.has-options:hover:before { + content: ""; + display: block; + width: 100%; + height: 6px; + position: absolute; + top: 34px; + left: 0; +} + +.header > .user > ul > li.dark.has-options:hover:before { background: rgba(0, 0, 0, 0.7); } +.header > .user > ul > li.attn.has-options:hover:before { background: rgba(215, 99, 76, 0.95); } + +/** + * Items for the dropdown menu. + */ +.header > .user > ul > li.has-options > ul > li { + float: left; + width: 50%; + padding: 4px; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +.header > .user > ul > li.has-options > ul > li > a { + display: block; + padding: 6px 14px; + color: #fff; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + border-radius: 2px; +} + +.header > .user > ul > li.attn.has-options > ul > li > a:hover { + background-color: #d15843; +} + +/** + * The header navigation is floated right and consists of nice squares, love the squares. + */ +.header > .navigation { + float: right; +} + +.header > .navigation > li { + float: left; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + border-radius: 2px; + margin: 0 0 0 3px; +} + +.header > .navigation > li > a { + display: block; + color: #999; + text-shadow: 0 1px 0 #fff; + text-decoration: none; + font-weight: bold; + font-size: 14px; + text-transform: lowercase; + padding: 8px 16px; +} + +.header > .navigation > li:hover { + background: rgba(0, 0, 0, 0.1); +} + +.header > .navigation > li.important { + background: rgba(215, 99, 76, 0.6); +} + +.header > .navigation > li.important:hover { + background: rgba(215, 99, 76, 0.8); +} + +.header > .navigation > li.selected { + background: rgba(0, 0, 0, 0.25); +} + +.header > .navigation > li.selected > a, +.header > .navigation > li.important > a { + color: #fff; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); +} + +/** + * The main body is a large container that stands out from the background. + */ +.body { + margin: 30px 0 0; + background-color: #fff; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.1); + padding: 10px 10px 20px; + -moz-border-radius: 0 0 2px 2px; + -webkit-border-radius: 0 0 2px 2px; + border-radius: 0 0 2px 2px; +} + +.body .content { + clear: both; +} + +/** + * The breadcrumbs get nice little arrow separators. + */ +.breadcrumbs { + margin: -10px -10px 10px; + background-color: #f2f2f2; + border-bottom: 1px solid #e6e6e6; + text-shadow: 0 1px 0 #fff; + -moz-border-radius: 2px 2px 0 0; + -webkit-border-radius: 2px 2px 0 0; + border-radius: 2px 2px 0 0; + line-height: 40px; +} + +.breadcrumbs > li, +.breadcrumbs > li > a { + color: #666; +} + +.breadcrumbs > li { + display: inline-block; + margin: 0 0 0 -3px; + cursor: default; +} + +.breadcrumbs > li:first-child { margin-left: 0; } + +.breadcrumbs > li, +.breadcrumbs > li > a { + position: relative; + padding: 0 4px 0 26px; + color: #989898; +} + +.breadcrumbs > li > a { + display: block; + text-decoration: none; + margin: 0 -4px 0 -26px; +} + +.breadcrumbs > li:first-child, +.breadcrumbs > li:first-child > a { + position: relative; + padding: 0 4px 0 10px; + color: #989898; +} + +.breadcrumbs > li:first-child > a { + margin: 0 -4px 0 -10px; +} + +.breadcrumbs > li > a:hover, +.breadcrumbs > li > a:focus { + color: #555; +} + +.breadcrumbs > li:hover { + background-color: #f9f9f9; +} + +.breadcrumbs > li:active { + border-color: #f1f1f1; +} + +.breadcrumbs > li:before { + position: absolute; + display: inline-block; + content: ""; + top: -1px; + right: -20px; + border-top: 21px solid rgba(255, 255, 255, 0); + border-bottom: 21px solid rgba(255, 255, 255, 0); + border-left: 19px solid #e2e2e2; + z-index: 1; +} + +.breadcrumbs > li:hover:before, +.breadcrumbs > li:focus:before { + border-left-color: #d7d7d7; +} + +.breadcrumbs > li:after { + position: absolute; + display: inline-block; + content: ""; + top: -1px; + right: -18px; + border-top: 21px solid rgba(255, 255, 255, 0); + border-bottom: 21px solid rgba(255, 255, 255, 0); + border-left: 19px solid #f2f2f2; + z-index: 1; +} + +.breadcrumbs > li:hover:after { + border-left-color: #f9f9f9; +} + +.breadcrumbs > li:hover { text-decoration: none; } + +/** + * Headings appear throughout the body and are a different font and weight + * then other items. + */ +.body h1, +.body h2, +.body h3, +.body h4 { + margin: 0 0 16px; + font-family: "Trebuchet MS", "Ubuntu", serif; + font-size: 24px; + color: #999; + text-transform: uppercase; +} + +.body h2 { font-size: 20px; } +.body h3 { font-size: 18px; } +.body h4 { + font-size: 14px; + display: inline; +} + +.body h1 a, +.body h2 a { + text-decoration: none; +} + +.body p:first-child { margin-top: 0; } +.body p:last-child { margin-bottom: 0; } + +/** + * Body halves allow the page to be split in two equal halves. + */ +.halves > .half-left { + float: left; + width: 49%; +} + +.halves > .half-right { + margin: 0 0 0 51%; +} + +/** + * The footer simply appears under the body and nothing more. + */ +.footer { + font-size: 11px; + padding: 10px 0; + color: #999; +} + +.footer > .powered { + float: left; + margin: 0; +} + +.footer > .stats { + float: right; +} + +.footer > .stats > li { + float: left; + margin: 0 0 0 24px; +} + +.footer > .stats .unit { + display: inline-block; + background-color: #999; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + color: #fff; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); + padding: 1px 4px; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2); + font-weight: bold; + margin: 0 4px 0 0; +} \ No newline at end of file diff --git a/app/themes/basic/public/img/background.png b/app/themes/basic/public/img/background.png new file mode 100644 index 0000000..3e3aca4 Binary files /dev/null and b/app/themes/basic/public/img/background.png differ diff --git a/app/themes/basic/public/img/heading.png b/app/themes/basic/public/img/heading.png new file mode 100644 index 0000000..256be5a Binary files /dev/null and b/app/themes/basic/public/img/heading.png differ diff --git a/app/themes/basic/public/img/loading.gif b/app/themes/basic/public/img/loading.gif new file mode 100644 index 0000000..cad72e6 Binary files /dev/null and b/app/themes/basic/public/img/loading.gif differ diff --git a/app/themes/basic/public/img/sprites.png b/app/themes/basic/public/img/sprites.png new file mode 100644 index 0000000..6768ab3 Binary files /dev/null and b/app/themes/basic/public/img/sprites.png differ diff --git a/app/themes/basic/start.php b/app/themes/basic/start.php new file mode 100644 index 0000000..45dc4d9 --- /dev/null +++ b/app/themes/basic/start.php @@ -0,0 +1,6 @@ +add('feather/themes/basic/css/theme.css'); +})->apply('UriRewriteFilter', array($app['path.public'])); \ No newline at end of file diff --git a/app/themes/basic/views/theme.blade.php b/app/themes/basic/views/theme.blade.php new file mode 100644 index 0000000..5b84081 --- /dev/null +++ b/app/themes/basic/views/theme.blade.php @@ -0,0 +1,60 @@ + + + + + + + @event('view: before template.title') Title + + + {{ Basset::show('basic.css') }} + + + + +
+ +
+ +
+ +
+ + + + + +
+ +
+ + +
+ +
+
+ + + +
+ + + + + + \ No newline at end of file diff --git a/app/views/hello.blade.php b/app/views/hello.php similarity index 100% rename from app/views/hello.blade.php rename to app/views/hello.php diff --git a/composer.json b/composer.json index 97c376c..37cafb4 100644 --- a/composer.json +++ b/composer.json @@ -1,13 +1,24 @@ { "require": { - "illuminate/foundation": ">=1.0.0" + "illuminate/foundation": ">=1.0.0", + "jasonlewis/basset": "*" + }, + "require-dev": { + "mockery/mockery": "0.7.*" }, "autoload": { "classmap": [ + "feather/Auth", + "feather/Console", + "feather/Database", + "feather/Extensions", + "feather/Migrator", + "feather/Presenter", "app/controllers", "app/models", + "app/extensions", "app/database/migrations", - "app/tests/TestCase.php" + "tests/TestCase.php" ] }, "minimum-stability": "dev" diff --git a/feather/Auth/AuthServiceProvider.php b/feather/Auth/AuthServiceProvider.php new file mode 100644 index 0000000..7c569d4 --- /dev/null +++ b/feather/Auth/AuthServiceProvider.php @@ -0,0 +1,26 @@ +app['auth']->extend('feather', function($app) + { + $provider = new FeatherUserProvider($app['hash'], $app['feather']['migrator']); + + return new Guard($provider, $app['session']); + }); + + $this->app['config']->set('auth.driver', 'feather'); + } + +} \ No newline at end of file diff --git a/feather/Auth/FeatherUserProvider.php b/feather/Auth/FeatherUserProvider.php new file mode 100644 index 0000000..3a7d79a --- /dev/null +++ b/feather/Auth/FeatherUserProvider.php @@ -0,0 +1,97 @@ +hasher = $hasher; + $this->migrator = $migrator; + } + + /** + * Retrieve a user by their unique idenetifier. + * + * @param mixed $identifier + * @return Illuminate\Auth\UserInterface|null + */ + public function retrieveByID($identifier) + { + $user = User::find($identifier); + + if ( ! is_null($user)) + { + return $user; + } + } + + /** + * Retrieve a user by the given credentials. + * + * @param array $credentials + * @return Illuminate\Auth\UserInterface|null + */ + public function retrieveByCredentials(array $credentials) + { + // First we'll attempt to locate a user by checking for the username they provided when logging in. + if ( ! $user = User::where('username', $credentials['username'])->first()) + { + return; + } + + // If we have a migrator interface then we need confirm that the user has been migrated previously. If + // the user has not been migrated then we'll leave it to the migrator to handle it. + if ($this->migrator->isActive()) + { + dd('here'); + } + + + if ( ! is_null($user)) + { + return $user; + } + } + + /** + * Validate a user against the given credentials. + * + * @param Illuminate\Auth\UserInterface $user + * @param array $credentials + * @return bool + */ + public function validateCredentials(UserInterface $user, array $credentials) + { + $plain = $credentials['password']; + + return $this->hasher->check($plain, $user->getAuthPassword()); + } + +} \ No newline at end of file diff --git a/feather/Console/FeatherCommand.php b/feather/Console/FeatherCommand.php new file mode 100644 index 0000000..dd85508 --- /dev/null +++ b/feather/Console/FeatherCommand.php @@ -0,0 +1,30 @@ +line('Feather version '.FEATHER_VERSION.''); + } +} \ No newline at end of file diff --git a/feather/Console/InstallCommand.php b/feather/Console/InstallCommand.php new file mode 100644 index 0000000..7cf0dab --- /dev/null +++ b/feather/Console/InstallCommand.php @@ -0,0 +1,30 @@ +line('Feather version '.FEATHER_VERSION.''); + } +} \ No newline at end of file diff --git a/feather/Console/PublishCommand.php b/feather/Console/PublishCommand.php new file mode 100644 index 0000000..9dfe8a5 --- /dev/null +++ b/feather/Console/PublishCommand.php @@ -0,0 +1,138 @@ +publisher = $publisher; + $this->themePath = $themePath; + $this->extensionPath = $extensionPath; + } + + /** + * Execute the console command. + * + * @return void + */ + public function fire() + { + // The publish command encompasses both themes and extensions. Because of this a flag must be provided so that the + // publisher knows what's being published and where it is located. + if ( ! $this->input->getOption('theme') and ! $this->input->getOption('extension')) + { + $this->comment("Please use either the --theme or --extension flag."); + + return; + } + + $name = $this->input->getArgument('name'); + + // Using the AssetPublisher we can publish our themes or extensions to the correct destination within our public + // directory. If publishing fails we'll catch the exception and let the user no that they probably spelt the + // theme or extension incorrectly. + + if ($this->input->getOption('theme')) + { + try + { + $this->publisher->publish("{$this->themePath}/{$name}/public", "../feather/themes/{$name}"); + + $this->line("Successfully published theme: {$name}"); + } + catch (RuntimeException $error) + { + $this->comment('Failed to publish theme. There may be nothing to publish.'); + } + } + elseif ($this->input->getOption('extension')) + { + try + { + $this->publisher->publish("{$this->extensionPath}/{$name}/public", "../feather/extensions/{$name}"); + + $this->line("Successfully published extension: {$name}"); + } + catch (RuntimeException $error) + { + $this->comment('Failed to publish extension. There may be nothing to publish.'); + } + } + } + + /** + * Get the command arguments. + * + * @return array + */ + public function getArguments() + { + return array( + array('name', InputArgument::REQUIRED, 'Name of the theme or extension to publish') + ); + } + + /** + * Get the command options. + * + * @return array + */ + public function getOptions() + { + return array( + array('theme', null, InputOption::VALUE_NONE, 'Tells publisher to publish a theme'), + array('extension', null, InputOption::VALUE_NONE, 'Tells publisher to publish an extension'), + ); + } + +} \ No newline at end of file diff --git a/feather/Database/EloquentBuilder.php b/feather/Database/EloquentBuilder.php new file mode 100644 index 0000000..a830db6 --- /dev/null +++ b/feather/Database/EloquentBuilder.php @@ -0,0 +1,34 @@ +model->cachable[$this->model->table])) + { + list($group, $foreign) = $this->model->cachable[$this->model->table]; + + if (Cache::has("{$group}_{$id}")) + { + return Cache::get("{$group}_{$id}"); + } + } + + $this->query->where($this->model->getKeyName(), '=', $id); + + return $this->first($columns); + } + +} \ No newline at end of file diff --git a/feather/Extensions/Dispatcher.php b/feather/Extensions/Dispatcher.php new file mode 100644 index 0000000..5d9af30 --- /dev/null +++ b/feather/Extensions/Dispatcher.php @@ -0,0 +1,263 @@ +files = $files; + $this->path = $path; + } + + /** + * Set the Laravel application instance. + * + * @param Illuminate\Foundation\Application $app + * @return void + */ + public function setApplication($app) + { + $this->app = $app; + } + + /** + * Register an array of extensions with the dispatcher. + * + * @param array $extensions + * @return void + */ + public function registerExtensions($extensions) + { + foreach ($extensions as $extension) + { + $this->register($extension); + } + } + + /** + * Get the started extensions. + * + * @return array + */ + public function getStarted() + { + return $this->started; + } + + /** + * Register an extension with the dispatcher. + * + * @param array $extension + * @return array + */ + public function register(array $extension) + { + $identifier = $extension['identifier']; + + $path = $this->path.'/'.$extension['location']; + + if ($this->files->exists($path)) + { + $extension['path'] = $path; + + $extension['loaded'] = array(); + + $this["extension.{$extension['identifier']}"] = $extension; + + // If an extension is set to be automatically started then we'll hand it off to + // the starting method. + if ($extension['auto']) + { + $this->start($extension['identifier']); + } + + return $this["extension.{$extension['identifier']}"]; + } + } + + /** + * Determine if an extension is regisetered. + * + * @param string $identifier + * @return bool + */ + public function isRegistered($identifier) + { + return isset($this["extension.{$identifier}"]); + } + + /** + * Determine if an extension is started. + * + * @param string $identifier + * @return bool + */ + public function isStarted($identifier) + { + return in_array($identifier, $this->started); + } + + /** + * Start an extension. + * + * @param string $identifier + * @return void + */ + public function start($identifier) + { + if ($this->isStarted($identifier) or ! $this->isRegistered($identifier)) + { + return; + } + + $extension = $this["extension.{$identifier}"]; + + foreach ($this->findExtensions($extension['path']) as $file) + { + $name = $file->getBasename(".{$file->getExtension()}"); + + if (ends_with($name, 'Extension')) + { + $location = str_replace('/', '\\', $extension['location']); + + $class = "Feather\\Extensions\\{$location}\\{$name}"; + + // Instantiate the new extension class and assign it to the extensions loaded classes. The class + // receives an instance of the Laravel application. + $extension['loaded'][$class] = $this->loadExtension($class); + + $extension['loaded'][$class]->start($this->app); + } + } + + $this["extension.{$identifier}"] = $extension; + + // Add the extension to the array of started extensions. + $this->started[] = $identifier; + } + + /** + * Get a FilesystemIterator to find the extensions within a path. + * + * @param string $path + * @return FilesystemIterator + */ + public function findExtensions($path) + { + return new FilesystemIterator($path); + } + + /** + * Create a new extension instance. + * + * @param string $class + * @return Feather\Extensions\Extension + */ + public function loadExtension($class) + { + return new $class($this->app); + } + + public function fire() + { + + } + + /** + * Determine if a given offset exists. + * + * @param string $key + * @return bool + */ + public function offsetExists($key) + { + return isset($this->extensions[$key]); + } + + /** + * Get the value at a given offset. + * + * @param string $key + * @return mixed + */ + public function offsetGet($key) + { + if ( ! isset($this->extensions[$key])) + { + throw new InvalidArgumentException("Type {$key} is not bound."); + } + + return $this->extensions[$key]; + } + + /** + * Set the value at a given offset. + * + * @param string $key + * @param mixed $value + * @return void + */ + public function offsetSet($key, $value) + { + $this->extensions[$key] = $value; + } + + /** + * Unset the value at a given offset. + * + * @param string $key + * @return void + */ + public function offsetUnset($key) + { + unset($this->extensions[$key]); + } + +} \ No newline at end of file diff --git a/feather/Extensions/Extension.php b/feather/Extensions/Extension.php new file mode 100644 index 0000000..010eec8 --- /dev/null +++ b/feather/Extensions/Extension.php @@ -0,0 +1,77 @@ +app = $app; + } + + /** + * Listen for an event and fire a method or closure handler. + * + * @param string $event + * @param string|Closure $handler + * @return void + */ + public function listen($event, $handler) + { + $this->bindEvent('listen', $event, $handler); + } + + /** + * Overrides existing events listening for an event and fire + * a method or closure handler. + * + * @param string $event + * @param string|Closure $handler + * @return void + */ + public function override($event, $handler) + { + $this->bindEvent('override', $event, $handler); + } + + /** + * Binds events for the extension. + * + * @param string $type + * @param string $event + * @param string|Closure $handler + * @return void + */ + private function bindEvent($type, $event, $handler) + { + // Set the current extension instance so we can use it within the event closure. + $extension = $this; + + $this->app['events']->$type($event, function($parameters = array()) use ($extension, $handler) + { + // If the handler is a callable closure then we'll execute the closure + // and return its result. + if (is_callable($handler)) + { + return $handler($parameters); + } + + // Finally assume that the handler is a method on the extension itself. + return call_user_func_array(array($extension, $handler), $parameters); + }); + } + +} \ No newline at end of file diff --git a/feather/Extensions/ExtensionInterface.php b/feather/Extensions/ExtensionInterface.php new file mode 100644 index 0000000..b5041d6 --- /dev/null +++ b/feather/Extensions/ExtensionInterface.php @@ -0,0 +1,41 @@ +app['feather.extensions'] = $this->app->share(function($app) + { + return new Dispatcher($app['files'], $app['path.extensions']); + }); + + $this->app['feather.extensions']->setApplication($this->app); + } + +} \ No newline at end of file diff --git a/feather/Migrator/DriverInterface.php b/feather/Migrator/DriverInterface.php new file mode 100644 index 0000000..9d014ff --- /dev/null +++ b/feather/Migrator/DriverInterface.php @@ -0,0 +1,33 @@ +app = $app; + } + + /** + * Set the migrator database record. + * + * @param Feather\Models\Migrator $migrator + */ + public function setMigratorRecord(Migrator $migrator) + { + $this->migratorRecord = $migrator; + + return $this; + } + + /** + * Determine if the driver has a migrator database record. + * + * @return bool + */ + public function hasMigratorRecord() + { + return ! is_null($this->migratorRecord); + } + +} \ No newline at end of file diff --git a/feather/Migrator/Drivers/FluxDriver.php b/feather/Migrator/Drivers/FluxDriver.php new file mode 100644 index 0000000..abf1211 --- /dev/null +++ b/feather/Migrator/Drivers/FluxDriver.php @@ -0,0 +1,35 @@ +app = $app; + $this->connection = $connection; + $this->active = $active; + } + + /** + * Get the default migrator driver name. + * + * @return string + */ + protected function getDefaultDriver() + { + return $this->app['config']->get('feather::migrator.driver'); + } + + /** + * Call a custom driver creator. + * + * @param string $driver + * @return mixed + */ + protected function callCustomCreator($driver) + { + return $this->customCreators[$driver]($this->app, $this->connection); + } + + /** + * Create a new flux driver instance. + * + * @return Feather\Migrator\Drivers\FluxDriver + */ + protected function createFluxDriver() + { + return new Drivers\FluxDriver($this->app); + } + + /** + * Determine if the migrator is active. + * + * @return bool + */ + public function isActive() + { + return $this->active; + } + +} \ No newline at end of file diff --git a/feather/Migrator/MigratorServiceProvider.php b/feather/Migrator/MigratorServiceProvider.php new file mode 100644 index 0000000..c940ce8 --- /dev/null +++ b/feather/Migrator/MigratorServiceProvider.php @@ -0,0 +1,48 @@ +app['feather.migrator'] = $this->app->share(function($app) + { + // Get the active migrator driver from the database. We'll then determine the connection to be used + // for the migrator. + $migrator = new Migrator; + + $migrator = $migrator->getMigratorDriver($app['config']->get('feather::migrator.driver')); + + // By default the database connection will be Feather's database connection. This is useful when you want + // the original tables from the old system to be on the same database as Feather. Migrators can overwrite + // this default connection. + $connection = $app['db']->connection(FEATHER_DATABASE); + + if ( ! is_null($migrator->options->database)) + { + $app['config']->set('database.connections.feather:migrator', array( + 'driver' => $migrator->options->driver, + 'host' => $migrator->options->host, + 'database' => $migrator->options->database, + 'username' => $migrator->options->username, + 'password' => $migrator->options->password, + 'charset' => $migrator->options->charset, + 'prefix' => $migrator->options->prefix, + 'collation' => $migrator->options->collation + )); + + $connection = $app['db']->connection('feather:migrator'); + } + + return new MigratorManager($app, $connection, ! is_null($migrator)); + }); + } + +} \ No newline at end of file diff --git a/feather/Presenter/Compilers/FeatherCompiler.php b/feather/Presenter/Compilers/FeatherCompiler.php new file mode 100644 index 0000000..f60f042 --- /dev/null +++ b/feather/Presenter/Compilers/FeatherCompiler.php @@ -0,0 +1,73 @@ +compilers = array_merge($this->compilers, array( + 'Assignments', + 'ExtensionEvents', + 'InlineErrors', + 'Errors' + )); + + return parent::compileString($value); + } + + /** + * Compile assignments into valid PHP. + * + * @param string $value + * @return string + */ + public function compileAssignments($value) + { + return preg_replace('/(\s*)@assign\s*\(\$(.*), (.*)\)(\s*)/', '$1$4', $value); + } + + /** + * Compile extension events into valid PHP. + * + * @param string $value + * @return string + */ + public function compileExtensionEvents($value) + { + $pattern = $this->createMatcher('event'); + + return preg_replace($pattern, '$1', $value); + } + + /** + * Compile inline errors into valid PHP. + * + * @param string $value + * @return string + */ + public function compileInlineErrors($value) + { + $pattern = $this->createMatcher('error'); + + return preg_replace($pattern, '$1has$2 ? view("feather::errors.inline", array("error" => $errors->first$2)) : null; ?>', $value); + } + + /** + * Compile errors into valid PHP. + * + * @param string $value + * @return string + */ + public function compileErrors($value) + { + return str_replace('@errors', 'all() ? view("feather::errors.page", array("errors" => $errors->all())) : null; ?>', $value); + } + +} \ No newline at end of file diff --git a/feather/Presenter/Presenter.php b/feather/Presenter/Presenter.php new file mode 100644 index 0000000..4a60c37 --- /dev/null +++ b/feather/Presenter/Presenter.php @@ -0,0 +1,54 @@ +app = $app; + } + + /** + * Prepare the theme by requiring a starter file if it exists. + * + * @return void + */ + public function prepare() + { + // If we are within theme development mode and not running within the console then we'll publish the current themes + // assets. This is especially handy when you're making a lot of changes to your themes assets. + if ($this->app['config']->get('feather.forum.theme_development_mode') and ! $this->app->runningInConsole()) + { + $this->app['artisan']->call('feather:publish', array('name' => $this->app['config']->get('feather::forum.theme'), '--theme' => true)); + } + + // Assign a namespace and some cascading paths so that view files are first searched + // for within a theme then within the core view directory. + $this->app['view']->addLocation($this->app['path.themes'].'/'.$this->app['config']->get('feather.forum.theme').'/views'); + $this->app['view']->addLocation($this->app['path'].'/views'); + + // If the theme has a start file require the file to bootstrap the theme. + $start = $this->app['path.themes'].'/'.$this->app['config']->get('feather.forum.theme').'/start.php'; + + if ($this->app['files']->exists($start)) + { + // We'll make $app available to our start scripts so that they can access bound data and components. + $app = $this->app; + + require $start; + } + } + +} \ No newline at end of file diff --git a/feather/Presenter/PresenterServiceProvider.php b/feather/Presenter/PresenterServiceProvider.php new file mode 100644 index 0000000..cb0e53a --- /dev/null +++ b/feather/Presenter/PresenterServiceProvider.php @@ -0,0 +1,47 @@ +app['feather.presenter'] = $this->app->share(function($app) + { + return new Presenter($app); + }); + + $this->registerCompiler(); + } + + /** + * Register the view compiler. + * + * @return void + */ + public function registerCompiler() + { + // Our compiler needs an instance of the file system and the storage path of the compiled views. + $files = $this->app['files']; + + $storagePath = $this->app['path'].'/storage/views'; + + $this->app['view']->addExtension('blade.php', 'feather.compiler', function() use ($files, $storagePath) + { + // FeatherCompiler is used by Feather is an extension to the Blade compiler. Feather + // has a few special methods that are used throughout views that need to be compiled + // alongside the default Blade methods. + $compiler = new Compilers\FeatherCompiler($files, $storagePath); + + return new CompilerEngine($compiler, $files); + }); + } + +} \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index a4c7609..992c249 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -12,7 +12,7 @@ > - ./app/tests/ + ./tests/ \ No newline at end of file diff --git a/start.php b/start.php index 6489453..869fe0a 100644 --- a/start.php +++ b/start.php @@ -2,7 +2,6 @@ use Illuminate\Filesystem; use Illuminate\Config\FileLoader; -use Illuminate\Config\ApcFileLoader; /* |-------------------------------------------------------------------------- diff --git a/tests/ExtensionDispatcherTest.php b/tests/ExtensionDispatcherTest.php new file mode 100644 index 0000000..9ca18c3 --- /dev/null +++ b/tests/ExtensionDispatcherTest.php @@ -0,0 +1,66 @@ +getDispatcher(); + $extension = array('location' => 'extension/location', 'identifier' => 'foo', 'auto' => false); + $dispatcher->register($extension); + $this->assertEquals('foo', $dispatcher['extension.foo']['identifier']); + $this->assertEquals('extension/location', $dispatcher['extension.foo']['location']); + $this->assertTrue($dispatcher->isRegistered('foo')); + } + + + public function testDispatcherRegistersExtensionsFromArray() + { + $dispatcher = $this->getDispatcher(); + $extension = array('location' => 'extension/location', 'identifier' => 'foo', 'auto' => false); + $dispatcher->registerExtensions(array($extension)); + $this->assertEquals('foo', $dispatcher['extension.foo']['identifier']); + $this->assertEquals('extension/location', $dispatcher['extension.foo']['location']); + } + + + public function testDispatcherAutoStartExtension() + { + $app = new Illuminate\Container; + $app['events'] = new Illuminate\Events\Dispatcher; + $files = m::mock('Illuminate\Filesystem'); + $files->shouldReceive('exists')->once()->andReturn(true); + $dispatcher = m::mock('Feather\Extensions\Dispatcher[findExtensions,loadExtension]'); + $dispatcher->__construct($files, 'path/to'); + $dispatcher->setApplication($app); + $extension = array('location' => 'extension/location', 'identifier' => 'foo', 'auto' => true); + $file = m::mock('stdClass'); + $file->shouldReceive('getBasename')->once()->andReturn('FooExtension'); + $file->shouldReceive('getExtension')->once()->andReturn('php'); + $instantiatedExtension = m::mock('Feather\Extensions\Extension'); + $instantiatedExtension->shouldReceive('start')->once(); + $dispatcher->shouldReceive('findExtensions')->once()->with('path/to/extension/location')->andReturn(array($file)); + $dispatcher->shouldReceive('loadExtension')->with('Feather\Extensions\extension\location\FooExtension')->andReturn($instantiatedExtension); + $dispatcher->register($extension); + $this->assertContains('foo', $dispatcher->getStarted()); + $this->assertTrue($dispatcher->isStarted('foo')); + } + + + protected function getDispatcher() + { + $app = new Illuminate\Container; + $app['events'] = new Illuminate\Events\Dispatcher; + $files = m::mock('Illuminate\Filesystem'); + $files->shouldReceive('exists')->once()->andReturn(true); + $dispatcher = m::mock(new Feather\Extensions\Dispatcher($files, 'path/to')); + $dispatcher->setApplication($app); + return $dispatcher; + } + + +} + diff --git a/tests/ExtensionTest.php b/tests/ExtensionTest.php new file mode 100644 index 0000000..fa0f45d --- /dev/null +++ b/tests/ExtensionTest.php @@ -0,0 +1,90 @@ +getApplicationAndDispatcher(); + $extension = array('location' => 'extension/location', 'identifier' => 'foo', 'auto' => true); + $dispatcher->register($extension); + $this->assertEquals('success', $app['events']->first('start_test')); + } + + + public function testExtensionsCanListenForEvents() + { + list($app, $dispatcher) = $this->getApplicationAndDispatcher(); + $extension = array('location' => 'extension/location', 'identifier' => 'foo', 'auto' => true); + $extension = $dispatcher->register($extension); + $extension['loaded']['Feather\Extensions\extension\location\FooExtension']->listen('foobar', function() + { + return 'barfoo'; + }); + $extension['loaded']['Feather\Extensions\extension\location\FooExtension']->listen('barfoo', function() + { + return 'foobar'; + }); + $this->assertEquals('barfoo', $app['events']->first('foobar')); + $this->assertEquals('foobar', $app['events']->first('barfoo')); + } + + + public function testExtensionsCanOverrideEvents() + { + list($app, $dispatcher) = $this->getApplicationAndDispatcher(); + $extension = array('location' => 'extension/location', 'identifier' => 'foo', 'auto' => true); + $extension = $dispatcher->register($extension); + $extension['loaded']['Feather\Extensions\extension\location\FooExtension']->listen('foobar', function() + { + return 'barfoo'; + }); + $extension['loaded']['Feather\Extensions\extension\location\FooExtension']->override('foobar', function() + { + return 'barbar'; + }); + $this->assertEquals('barbar', $app['events']->first('foobar')); + } + + + public function testExtensionsCanUseMethods() + { + list($app, $dispatcher) = $this->getApplicationAndDispatcher(); + $extension = array('location' => 'extension/location', 'identifier' => 'foo', 'auto' => true); + $extension = $dispatcher->register($extension); + $extension['loaded']['Feather\Extensions\extension\location\FooExtension']->listen('foobar', 'foo'); + $this->assertEquals('foomethod', $app['events']->first('foobar')); + } + + + protected function getApplicationAndDispatcher() + { + $app = new Illuminate\Container; + $app['events'] = new Illuminate\Events\Dispatcher; + $app['files'] = m::mock('Illuminate\Filesystem'); + $app['files']->shouldReceive('exists')->once()->andReturn(true); + $dispatcher = m::mock('Feather\Extensions\Dispatcher[findExtensions,loadExtension]'); + $dispatcher->__construct($app['files'], 'path/to'); + $dispatcher->setApplication($app); + $file = m::mock('stdClass'); + $file->shouldReceive('getBasename')->once()->andReturn('FooExtension'); + $file->shouldReceive('getExtension')->once()->andReturn('php'); + $instantiatedExtension = m::mock('Feather\Extensions\Extension[start]'); + $instantiatedExtension->__construct($app); + $instantiatedExtension->shouldReceive('foo')->andReturn('foomethod'); + $instantiatedExtension->shouldReceive('start')->once()->andReturnUsing(function() use ($instantiatedExtension) + { + $instantiatedExtension->listen('start_test', function() + { + return 'success'; + }); + }); + $dispatcher->shouldReceive('findExtensions')->once()->with('path/to/extension/location')->andReturn(array($file)); + $dispatcher->shouldReceive('loadExtension')->with('Feather\Extensions\extension\location\FooExtension')->andReturn($instantiatedExtension); + return array($app, $dispatcher); + } + + +} \ No newline at end of file diff --git a/tests/FeatherCompilerTest.php b/tests/FeatherCompilerTest.php new file mode 100644 index 0000000..a8c0e8d --- /dev/null +++ b/tests/FeatherCompilerTest.php @@ -0,0 +1,46 @@ +getFiles(), __DIR__); + $this->assertEquals('', $compiler->compileString('@assign($foo, \'bar\')')); + $this->assertEquals('', $compiler->compileString('@assign($foo, $bar)')); + } + + + public function testExtensionEventsAreCompiled() + { + $compiler = new FeatherCompiler($this->getFiles(), __DIR__); + $expected = ''; + $this->assertEquals($expected, $compiler->compileString('@event(\'foo\')')); + } + + + public function testInlineErrorsAreCompiled() + { + $compiler = new FeatherCompiler($this->getFiles(), __DIR__); + $expected = 'has(\'foo\') ? view("feather::errors.inline", array("error" => $errors->first(\'foo\'))) : null; ?>'; + $this->assertEquals($expected, $compiler->compileString('@error(\'foo\')')); + } + + + public function testErrorsAreCompiled() + { + $compiler = new FeatherCompiler($this->getFiles(), __DIR__); + $expected = 'all() ? view("feather::errors.page", array("errors" => $errors->all())) : null; ?>'; + $this->assertEquals($expected, $compiler->compileString('@errors')); + } + + + public function getFiles() + { + return m::mock('Illuminate\Filesystem'); + } + +} \ No newline at end of file diff --git a/tests/PresenterTest.php b/tests/PresenterTest.php new file mode 100644 index 0000000..791065c --- /dev/null +++ b/tests/PresenterTest.php @@ -0,0 +1,26 @@ +getLoader()->shouldReceive('load')->once()->with('production', 'feather', null)->once()->andReturn(array('forum' => array('theme' => 'foo'))); + $app['files'] = m::mock('Illuminate\Filesystem'); + $app['files']->shouldReceive('exists')->once()->andReturn(false); + $app['view'] = m::mock('Illuminate\View\ViewManager'); + $app['view']->shouldReceive('addLocation')->once()->with('themes/foo/views'); + $app['view']->shouldReceive('addLocation')->once()->with('app/views'); + $app['path'] = 'app'; + $app['path.themes'] = 'themes'; + $presenter = new Presenter($app); + $presenter->prepare(); + } + + +} \ No newline at end of file diff --git a/app/tests/TestCase.php b/tests/TestCase.php similarity index 61% rename from app/tests/TestCase.php rename to tests/TestCase.php index 1af7071..32d352b 100644 --- a/app/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,7 +1,21 @@