diff --git a/application/bundle.php b/application/bundle.php new file mode 100644 index 00000000..d1b9e362 --- /dev/null +++ b/application/bundle.php @@ -0,0 +1,42 @@ + APP_PATH.'models/user.php', + //'Role' => APP_PATH.'models/role.php', +)); \ No newline at end of file diff --git a/application/composers.php b/application/composers.php deleted file mode 100644 index c806c5fa..00000000 --- a/application/composers.php +++ /dev/null @@ -1,47 +0,0 @@ - array('name' => 'home') - | - | Now, you can create an instance of that view using the very expressive - | View::of dynamic method. Take a look at this example: - | - | return View::of_home(); - | - | View composers provide a convenient way to add common elements to a view - | each time it is created. For example, you may wish to bind a header and - | footer partial each time the view is created. - | - | The composer will receive an instance of the view being created, and is - | free to modify the view however you wish. Here is how to define one: - | - | 'home.index' => function($view) - | { - | // - | } - | - | Of course, you may define a view name and a composer for a single view: - | - | 'home.index' => array('name' => 'home', function($view) - | { - | // - | }) - | - */ - - 'home.index' => array('name' => 'home', function($view) - { - // This composer is called for the "home.index" view. - }), - -); \ No newline at end of file diff --git a/application/config/application.php b/application/config/application.php index 1c112ad6..b20fcd49 100644 --- a/application/config/application.php +++ b/application/config/application.php @@ -7,7 +7,9 @@ | Application URL |-------------------------------------------------------------------------- | - | The URL used to access your application. No trailing slash. + | The URL used to access your application without a trailing slash. The URL + | does nto have to be set. If it isn't we'll try our best to guess the URL + | of your application. | */ @@ -20,8 +22,8 @@ | | If you are including the "index.php" in your URLs, you can ignore this. | - | However, if you are using mod_rewrite or something similar to get - | cleaner URLs, set this option to an empty string. + | However, if you are using mod_rewrite to get cleaner URLs, just set + | this option to an empty string and we'll take care of the rest. | */ @@ -32,11 +34,10 @@ | Application Key |-------------------------------------------------------------------------- | - | The application key should be a random, 32 character string. - | | This key is used by the encryption and cookie classes to generate secure | encrypted strings and hashes. It is extremely important that this key - | remain secret and should not be shared with anyone. + | remain secret and should not be shared with anyone. Make it about 32 + | characters of random gibberish. | */ @@ -48,7 +49,8 @@ |-------------------------------------------------------------------------- | | The default character encoding used by your application. This encoding - | will be used by the Str, Text, and Form classes. + | will be used by the Str, Text, Form, and any other classes that need + | to know what type of encoding to use for your awesome application. | */ @@ -89,12 +91,28 @@ |-------------------------------------------------------------------------- | | The default timezone of your application. This timezone will be used when - | Laravel needs a date, such as when writing to a log file. + | Laravel needs a date, such as when writing to a log file or travelling + | to a distant star at warp speed. | */ 'timezone' => 'UTC', + /* + |-------------------------------------------------------------------------- + | Autoloaded Bundles + |-------------------------------------------------------------------------- + | + | Bundles can provide a ton of awesome drop-in functionality for your web + | application. Everything from Twitter integration to an admin backend. + | + | Here you may specify the bundles that should be automatically started + | on every request to your application. + | + */ + + 'bundles' => array(), + /* |-------------------------------------------------------------------------- | Class Aliases @@ -112,26 +130,26 @@ */ 'aliases' => array( - 'Arr' => 'Laravel\\Arr', - 'Asset' => 'Laravel\\Asset', 'Auth' => 'Laravel\\Auth', + 'Asset' => 'Laravel\\Asset', 'Autoloader' => 'Laravel\\Autoloader', - 'Benchmark' => 'Laravel\\Benchmark', - 'Cache' => 'Laravel\\Cache\\Manager', + 'Bundle' => 'Laravel\\Bundle', + 'Cache' => 'Laravel\\Cache', 'Config' => 'Laravel\\Config', 'Controller' => 'Laravel\\Routing\\Controller', 'Cookie' => 'Laravel\\Cookie', 'Crypter' => 'Laravel\\Crypter', - 'DB' => 'Laravel\\Database\\Manager', - 'Eloquent' => 'Laravel\\Database\\Eloquent\\Model', + 'DB' => 'Laravel\\Database', + 'Event' => 'Laravel\\Event', 'File' => 'Laravel\\File', + 'Filter' => 'Laravel\\Routing\\Filter', 'Form' => 'Laravel\\Form', 'Hash' => 'Laravel\\Hash', 'HTML' => 'Laravel\\HTML', - 'Inflector' => 'Laravel\\Inflector', 'Input' => 'Laravel\\Input', 'IoC' => 'Laravel\\IoC', 'Lang' => 'Laravel\\Lang', + 'Log' => 'Laravel\\Log', 'Memcached' => 'Laravel\\Memcached', 'Paginator' => 'Laravel\\Paginator', 'URL' => 'Laravel\\URL', @@ -139,9 +157,13 @@ 'Redis' => 'Laravel\\Redis', 'Request' => 'Laravel\\Request', 'Response' => 'Laravel\\Response', + 'Router' => 'Laravel\\Routing\\Router', + 'Schema' => 'Laravel\\Database\\Schema', 'Section' => 'Laravel\\Section', - 'Session' => 'Laravel\\Facades\\Session', + 'Session' => 'Laravel\\Session', 'Str' => 'Laravel\\Str', + 'Task' => 'Laravel\\CLI\\Tasks\\Task', + 'URI' => 'Laravel\\URI', 'Validator' => 'Laravel\\Validator', 'View' => 'Laravel\\View', ), diff --git a/application/config/auth.php b/application/config/auth.php index c4671f01..36343d4f 100644 --- a/application/config/auth.php +++ b/application/config/auth.php @@ -2,45 +2,27 @@ return array( - /* - |-------------------------------------------------------------------------- - | Authentication Username - |-------------------------------------------------------------------------- - | - } This option should be set to the "username" property of your users. - | Typically, this will be set to "email" or "username". - | - | The value of this property will be used by the "attempt" closure when - | searching for users by their username. It will also be used when the - | user is set to be "remembered", as the username is embedded into the - | encrypted cookie and is used to verify the user's identity. - | - */ - - 'username' => 'email', - /* |-------------------------------------------------------------------------- | Retrieve The Current User |-------------------------------------------------------------------------- | - | This closure is called by the Auth::user() method when attempting to - | retrieve a user by their ID stored in the session. + | This closure is called by the Auth class' "user" method when trying to + | retrieve a user by the ID that is stored in their session. If you find + | the user, just return the user object, but make sure it has an "id" + | property. If you can't find the user, just return null. | - | Simply return an object representing the user with the given ID. Or, if - | no user with the given ID is registered to use your application, you do - | not need to return anything. - | - | Of course, a simple, elegant authentication solution is already provided - | for you using Eloquent and the default Laravel hashing engine. + | Of course, a simple and elegant authentication solution has already + | been provided for you using the query builder and hashing engine. + | We love making your life as easy as possible. | */ 'user' => function($id) { - if ( ! is_null($id) and filter_var($id, FILTER_VALIDATE_INT) !== false) + if (filter_var($id, FILTER_VALIDATE_INT) !== false) { - return User::find($id); + return DB::table('users')->find($id); } }, @@ -50,19 +32,19 @@ |-------------------------------------------------------------------------- | | This closure is called by the Auth::attempt() method when attempting to - | authenticate a user that is logging into your application. + | authenticate a user that is logging into your application. It's like a + | super buff bouncer to your application. | | If the provided credentials are correct, simply return an object that - | represents the user being authenticated. If the credentials are not - | valid, don't return anything. - | - | Note: If a user object is returned, it must have an "id" property. + | represents the user being authenticated. As long as it has a property + | for the "id", any object will work. If the credentials are not valid, + | you don't meed to return anything. | */ - 'attempt' => function($username, $password, $config) + 'attempt' => function($username, $password) { - $user = User::where($config['username'], '=', $username)->first(); + $user = DB::table('users')->where_username($username)->first(); if ( ! is_null($user) and Hash::check($password, $user->password)) { @@ -72,12 +54,12 @@ /* |-------------------------------------------------------------------------- - | Logout + | Logout The Current User |-------------------------------------------------------------------------- | | Here you may do anything that needs to be done when a user logs out of | your application, such as call the logout method on a third-party API - | you are using for authentication, or anything else you desire. + | you are using for authentication or anything else you desire. | */ diff --git a/application/config/cache.php b/application/config/cache.php index abd07eec..73fd4c9c 100644 --- a/application/config/cache.php +++ b/application/config/cache.php @@ -7,12 +7,15 @@ | Cache Driver |-------------------------------------------------------------------------- | - | The name of the default cache driver for your application. + | The name of the default cache driver for your application. Caching can + | be used to increase the performance of your application by storing any + | commonly accessed data in memory, a file, or some other storage. | - | Caching can be used to increase the performance of your application - | by storing commonly accessed data in memory or in a file. + | A variety of awesome drivers are available for you to use with Laravel. + | Some, like APC, are extremely fast. However, if that isn't an option + | in your environment, try file or database caching. | - | Supported Drivers: 'file', 'memcached', 'apc', 'redis'. + | Drivers: 'file', 'memcached', 'apc', 'redis', 'database'. | */ @@ -23,25 +26,39 @@ | Cache Key |-------------------------------------------------------------------------- | - | This key will be prepended to item keys stored using Memcached and APC to - | prevent collisions with other applications on the server. + | This key will be prepended to item keys stored using Memcached and APC + | to prevent collisions with other applications on the server. Since the + | memory based stores could be shared by other applications, we need to + | be polite and use a prefix to uniquely identifier our items. | */ 'key' => 'laravel', + /* + |-------------------------------------------------------------------------- + | Cache Database + |-------------------------------------------------------------------------- + | + | When using the database cache driver, this database table will be used + | to store the cached item. You may also add a "connection" option to + | the array to specify which database connection should be used. + | + */ + + 'database' => array('table' => 'laravel_cache'), + /* |-------------------------------------------------------------------------- | Memcached Servers |-------------------------------------------------------------------------- | - | The Memcached servers used by your application. + | The Memcached servers used by your application. Memcached is a free and + | open source, high-performance, distributed memory caching system. It is + | generic in nature but intended for use in speeding up web applications + | by alleviating database load. | - | Memcached is a free and open source, high-performance, distributed memory - | object caching system, generic in nature, but intended for use in speeding - | up dynamic web applications by alleviating database load. - | - | For more information about Memcached, check out: http://memcached.org + | For more information, check out: http://memcached.org | */ diff --git a/application/config/container.php b/application/config/container.php deleted file mode 100644 index 2e9b5ef0..00000000 --- a/application/config/container.php +++ /dev/null @@ -1,54 +0,0 @@ - function($c) - | { - | return new Mailer($sender, $key); - | } - | - | Note that the container instance itself is passed into the resolver, - | allowing you to continue to resolve dependencies within the resolver - | itself. This allows you to easily resolve nested dependencies. - | - | When creating controller instances, Laravel will check to see if a - | resolver has been registered for the controller. If it has, it will - | be used to create the controller instance. All controller resolvers - | should be registered beginning using a {controllers}.{name} naming - | convention. For example: - | - | 'controllers.user' => function($c) - | { - | return new User_Controller($c->resolve('repository')); - | } - | - | Of course, sometimes you may wish to register an object as a singleton - | Singletons are resolved by the controller the first time they are - | resolved; however, that same resolved instance will continue to be - | returned by the container each time it is requested. Registering an - | object as a singleton couldn't be simpler: - | - | 'mailer' => array('singleton' => true, 'resolver' => function($c) - | { - | return new Mailer($sender, $key); - | }) - | - */ - -); \ No newline at end of file diff --git a/application/config/database.php b/application/config/database.php index ebcac4d9..f2ba145e 100644 --- a/application/config/database.php +++ b/application/config/database.php @@ -7,39 +7,29 @@ | Default Database Connection |-------------------------------------------------------------------------- | - | The name of your default database connection. - | - | This connection will be the default for all database operations unless a - | different connection is specified when performing the operation. + | The name of your default database connection. This connection will used + | as the default for all database operations unless a different name is + | given when performing said operation. This connection name should be + | listed in the array of connections below. | */ - 'default' => 'sqlite', + 'default' => 'mysql', /* |-------------------------------------------------------------------------- | Database Connections |-------------------------------------------------------------------------- | - | All of the database connections used by your application. + | All of the database connections used by your application. Many of your + | applications will no doubt only use one connection; however, you have + | the freedom to specify as many connections as you can handle. | - | Supported Drivers: 'mysql', 'pgsql', 'sqlite'. + | All database work in Laravel is done through the PHP's PDO facilities, + | so make sure you have the PDO drivers for your particlar database of + | choice installed on your machine. | - | Note: When using the SQLite driver, the path and "sqlite" extention will - | be added automatically. You only need to specify the database name. - | - | Using a driver that isn't supported? You can still establish a PDO - | connection. Simply specify a driver and DSN option: - | - | 'odbc' => array( - | 'driver' => 'odbc', - | 'dsn' => 'your-dsn', - | 'username' => 'username', - | 'password' => 'password', - | ) - | - | Note: When using an unsupported driver, Eloquent and the fluent query - | builder may not work as expected. + | Drivers: 'mysql', 'pgsql', 'sqlsrv', 'sqlite'. | */ @@ -68,6 +58,14 @@ 'charset' => 'utf8', ), + 'sqlsrv' => array( + 'driver' => 'sqlsrv', + 'host' => 'localhost', + 'database' => 'database', + 'username' => 'root', + 'password' => 'password', + ), + ), /* @@ -77,11 +75,9 @@ | | Redis is an open source, fast, and advanced key-value store. However, it | provides a richer set of commands than a typical key-value store such as - | APC or memcached. + | APC or memcached. All the cool kids are using it. | - | Here you may specify the hosts and ports for your Redis databases. - | - | For more information regarding Redis, check out: http://redis.io + | To get the scoop on Redis, check out: http://redis.io | */ diff --git a/application/config/error.php b/application/config/error.php index 881ce8dd..87db9665 100644 --- a/application/config/error.php +++ b/application/config/error.php @@ -7,7 +7,7 @@ | Ignored Error Levels |-------------------------------------------------------------------------- | - | Here you may specify the error levels that should be ignored by the + | Here you simply specify the error levels that should be ignored by the | Laravel error handler. These levels will still be logged; however, no | information about about them will be displayed. | @@ -22,10 +22,11 @@ | | Detailed error messages contain information about the file in which an | error occurs, as well as a PHP stack trace containing the call stack. + | You'll want them when you're trying to debug your application. | - | If your application is in production, consider turning off error details - | for enhanced security and user experience. The error stack trace could - | contain sensitive information that should not be publicly visible. + | If your application is in production, you'll want to turn off the error + | details for enhanced security and user experience since the exception + | stack trace could contain sensitive information. | */ @@ -56,18 +57,13 @@ | | You may log the error message however you like; however, a simple log | solution has been setup for you which will log all error messages to - | a single text file within the application storage directory. - | - | Of course, you are free to implement more complex solutions including - | emailing the exceptions details to your team, etc. + | text files within the application storage directory. | */ 'logger' => function($exception) { - $message = (string) $exception; - - File::append(STORAGE_PATH.'log.txt', date('Y-m-d H:i:s').' - '.$message.PHP_EOL); + Log::exception($exception); }, ); \ No newline at end of file diff --git a/laravel/config/mimes.php b/application/config/mimes.php similarity index 100% rename from laravel/config/mimes.php rename to application/config/mimes.php diff --git a/application/config/session.php b/application/config/session.php index c72a3dd4..3b6e6a69 100644 --- a/application/config/session.php +++ b/application/config/session.php @@ -7,12 +7,12 @@ | Session Driver |-------------------------------------------------------------------------- | - | The name of the session driver for your application. + | The name of the session driver used by your application. Since HTTP is + | stateless, sessions are used to simulate "state" across requests made + | by the same user of your application. In other words, it's how an + | application knows who the heck you are. | - | Since HTTP is stateless, sessions are used to maintain "state" across - | multiple requests from the same user of your application. - | - | Supported Drivers: 'cookie', 'file', 'database', 'memcached', 'apc', 'redis'. + | Drivers: 'cookie', 'file', 'database', 'memcached', 'apc', 'redis'. | */ @@ -23,9 +23,9 @@ | Session Database |-------------------------------------------------------------------------- | - | The database table on which the session should be stored. - | - | This option is only relevant when using the "database" session driver. + | The database table on which the session should be stored. It probably + | goes without saying that this option only matters if you are using + | the super slick database session driver. | */ @@ -40,8 +40,9 @@ | This option specifies the probability of session garbage collection | occuring for any given request. | - | For example, the default value states that garbage collection has about - | a 2% (2 / 100) chance of occuring for any given request. + | For example, the default value states that garbage collection has a + | 2% chance of occuring for any given request to the application. + | Feel free to tune this to your application's size and speed. | */ diff --git a/application/config/strings.php b/application/config/strings.php new file mode 100644 index 00000000..3ee44f56 --- /dev/null +++ b/application/config/strings.php @@ -0,0 +1,119 @@ + array( + + 'user' => 'users', + 'person' => 'people', + 'comment' => 'comments', + + ), + + /* + |-------------------------------------------------------------------------- + | ASCII Characters + |-------------------------------------------------------------------------- + | + | This array contains foreign characters and their 7-bit ASCII equivalents. + | The array is used by the "ascii" method on the Str class to get strings + | ready for inclusion in a URL slug. + | + | Of course, the "ascii" method may also be used by you for whatever your + | application requires. Feel free to add any characters we missed, and be + | sure to let us know about them! + | + */ + + 'ascii' => array( + + '/æ|ǽ/' => 'ae', + '/œ/' => 'oe', + '/À|Á|Â|Ã|Ä|Å|Ǻ|Ā|Ă|Ą|Ǎ|А/' => 'A', + '/à|á|â|ã|ä|å|ǻ|ā|ă|ą|ǎ|ª|а/' => 'a', + '/Б/' => 'B', + '/б/' => 'b', + '/Ç|Ć|Ĉ|Ċ|Č|Ц/' => 'C', + '/ç|ć|ĉ|ċ|č|ц/' => 'c', + '/Ð|Ď|Đ|Д/' => 'Dj', + '/ð|ď|đ|д/' => 'dj', + '/È|É|Ê|Ë|Ē|Ĕ|Ė|Ę|Ě|Е|Ё|Э/' => 'E', + '/è|é|ê|ë|ē|ĕ|ė|ę|ě|е|ё|э/' => 'e', + '/Ф/' => 'F', + '/ƒ|ф/' => 'f', + '/Ĝ|Ğ|Ġ|Ģ|Г/' => 'G', + '/ĝ|ğ|ġ|ģ|г/' => 'g', + '/Ĥ|Ħ|Х/' => 'H', + '/ĥ|ħ|х/' => 'h', + '/Ì|Í|Î|Ï|Ĩ|Ī|Ĭ|Ǐ|Į|İ|И/' => 'I', + '/ì|í|î|ï|ĩ|ī|ĭ|ǐ|į|ı|и/' => 'i', + '/Ĵ|Й/' => 'J', + '/ĵ|й/' => 'j', + '/Ķ|К/' => 'K', + '/ķ|к/' => 'k', + '/Ĺ|Ļ|Ľ|Ŀ|Ł|Л/' => 'L', + '/ĺ|ļ|ľ|ŀ|ł|л/' => 'l', + '/М/' => 'M', + '/м/' => 'm', + '/Ñ|Ń|Ņ|Ň|Н/' => 'N', + '/ñ|ń|ņ|ň|ʼn|н/' => 'n', + '/Ö|Ò|Ó|Ô|Õ|Ō|Ŏ|Ǒ|Ő|Ơ|Ø|Ǿ|О/' => 'O', + '/ö|ò|ó|ô|õ|ō|ŏ|ǒ|ő|ơ|ø|ǿ|º|о/' => 'o', + '/П/' => 'P', + '/п/' => 'p', + '/Ŕ|Ŗ|Ř|Р/' => 'R', + '/ŕ|ŗ|ř|р/' => 'r', + '/Ś|Ŝ|Ş|Š|С/' => 'S', + '/ś|ŝ|ş|š|ſ|с/' => 's', + '/Ţ|Ť|Ŧ|Т/' => 'T', + '/ţ|ť|ŧ|т/' => 't', + '/Ù|Ú|Û|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ|У/' => 'U', + '/ù|ú|û|ũ|ū|ŭ|ů|ű|ų|ư|ǔ|ǖ|ǘ|ǚ|ǜ|у/' => 'u', + '/В/' => 'V', + '/в/' => 'v', + '/Ý|Ÿ|Ŷ|Ы/' => 'Y', + '/ý|ÿ|ŷ|ы/' => 'y', + '/Ŵ/' => 'W', + '/ŵ/' => 'w', + '/Ź|Ż|Ž|З/' => 'Z', + '/ź|ż|ž|з/' => 'z', + '/Æ|Ǽ/' => 'AE', + '/ß/'=> 'ss', + '/IJ/' => 'IJ', + '/ij/' => 'ij', + '/Œ/' => 'OE', + '/Ч/' => 'Ch', + '/ч/' => 'ch', + '/Ю/' => 'Ju', + '/ю/' => 'ju', + '/Я/' => 'Ja', + '/я/' => 'ja', + '/Ш/' => 'Sh', + '/ш/' => 'sh', + '/Щ/' => 'Shch', + '/щ/' => 'shch', + '/Ж/' => 'Zh', + '/ж/' => 'zh', + + ), + +); \ No newline at end of file diff --git a/application/controllers/home.php b/application/controllers/home.php index 982cc513..cc407246 100644 --- a/application/controllers/home.php +++ b/application/controllers/home.php @@ -7,10 +7,10 @@ class Home_Controller extends Controller { | The Default Controller |-------------------------------------------------------------------------- | - | Instead of using RESTful routes and anonymous functions, you may wish to - | use controllers to organize your application API. You'll love them. + | Instead of using RESTful routes and anonymous functions, you might wish + | to use controllers to organize your application API. You'll love them. | - | To start using this controller, simply remove the default route from the + | To start using this controller simply remove the default route from the | application "routes.php" file. Laravel is smart enough to find this | controller and call the default method, which is "action_index". | diff --git a/application/filters.php b/application/filters.php deleted file mode 100644 index 41ac6010..00000000 --- a/application/filters.php +++ /dev/null @@ -1,68 +0,0 @@ - function() - | { - | return 'Filtered!'; - | } - | - | Next, attach the filter to a route: - | - | 'GET /' => array('before' => 'simple_filter', function() - | { - | return 'Hello World!'; - | }) - | - | Now every requests to http://example.com will return "Filtered!", since - | the filter is overriding the route action by returning a value. - | - | To make your life easier, we have built authentication and CSRF filters - | that are ready to attach to your routes. Enjoy. - | - */ - - 'before' => function() - { - // Do stuff before every request to your application. - }, - - - 'after' => function($response) - { - // Do stuff after every request to your application. - }, - - - 'auth' => function() - { - if (Auth::guest()) return Redirect::to_login(); - }, - - - 'csrf' => function() - { - if (Request::forged()) return Response::error('500'); - }, - -); \ No newline at end of file diff --git a/application/language/en/pagination.php b/application/language/en/pagination.php index 8c0b694f..f58ada06 100644 --- a/application/language/en/pagination.php +++ b/application/language/en/pagination.php @@ -8,8 +8,8 @@ |-------------------------------------------------------------------------- | | The following language lines are used by the paginator library to build - | the pagination links. They may be easily changed by the developer to - | anything they wish. + | the pagination links. You're free to change them to anything you want. + | If you come up with something more exciting, let us know. | */ diff --git a/application/language/en/validation.php b/application/language/en/validation.php index a8e6d3c6..71553c5a 100644 --- a/application/language/en/validation.php +++ b/application/language/en/validation.php @@ -8,11 +8,12 @@ |-------------------------------------------------------------------------- | | The following language lines are used to swap attribute place-holders - | with something more reader friendly, such as "E-Mail Address" instead - | of "email". + | with something more reader friendly such as "E-Mail Address" instead + | of "email". Your users will thank you. | - | The Validator class will automatically search this array of lines when - | attempting to replace the :attribute place-holder in error messages. + | The Validator class will automatically search this array of lines it + | is attempting to replace the :attribute place-holder in messages. + | It's pretty slick. We think you'll like it. | */ @@ -28,9 +29,9 @@ | such as the size (max, min, between) rules. These versions are used | for different input types such as strings and files. | - | These language lines may be easily changed by the developer to provide - | custom error messages in their application. Error messages for custom - | validation rules may also be added to this file. + | These language lines may be easily changed to provide custom error + | messages in your application. Error messages for custom validation + | rules may also be added to this file. | */ diff --git a/application/migrations/.gitignore b/application/migrations/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/application/routes.php b/application/routes.php index 7bb7acd6..7bfd2439 100644 --- a/application/routes.php +++ b/application/routes.php @@ -1,42 +1,87 @@ function() - | { - | return 'Hello World!'; - | } - | - | You can even respond to more than one URI: - | - | 'GET /hello, GET /world' => function() - | { - | return 'Hello World!'; - | } - | - | It's easy to allow URI wildcards using (:num) or (:any): - | - | 'GET /hello/(:any)' => function($name) - | { - | return "Welcome, $name."; - | } - | - */ +Router::register(array('GET /', 'GET /home'), function() +{ + return View::make('home.index'); +}); - 'GET /' => function() - { - return View::make('home.index'); - }, +/* +|-------------------------------------------------------------------------- +| Route Filters +|-------------------------------------------------------------------------- +| +| Filters provide a convenient method for attaching functionality to your +| routes. The built-in "before" and "after" filters are called before and +| after every request to your application, and you may even create other +| filters that can be attached to individual routes. +| +| Let's walk through an example... +| +| First, define a filter: +| +| Filter::register('filter', function() +| { +| return 'Filtered!'; +| }); +| +| Next, attach the filter to a route: +| +| Router::register('GET /', array('before' => 'filter', function() +| { +| return 'Hello World!'; +| })); +| +*/ -); \ No newline at end of file +Filter::register('before', function() +{ + // Do stuff before every request to your application... +}); + +Filter::register('after', function() +{ + // Do stuff after every request to your application... +}); + +Filter::register('csrf', function() +{ + if (Request::forged()) return Response::error('500'); +}); + +Filter::register('auth', function() +{ + if (Auth::guest()) return Redirect::to('login'); +}); \ No newline at end of file diff --git a/application/storage/cache/.gitignore b/application/storage/cache/.gitignore deleted file mode 100644 index f59ec20a..00000000 --- a/application/storage/cache/.gitignore +++ /dev/null @@ -1 +0,0 @@ -* \ No newline at end of file diff --git a/application/storage/sessions/.gitignore b/application/storage/sessions/.gitignore deleted file mode 100644 index f59ec20a..00000000 --- a/application/storage/sessions/.gitignore +++ /dev/null @@ -1 +0,0 @@ -* \ No newline at end of file diff --git a/application/storage/views/.gitignore b/application/storage/views/.gitignore deleted file mode 100644 index f59ec20a..00000000 --- a/application/storage/views/.gitignore +++ /dev/null @@ -1 +0,0 @@ -* \ No newline at end of file diff --git a/application/tasks/.gitignore b/application/tasks/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/artisan b/artisan new file mode 100644 index 00000000..f31604ac --- /dev/null +++ b/artisan @@ -0,0 +1,49 @@ + + * @link http://laravel.com + */ + +// -------------------------------------------------------------- +// Define the directory separator for the environment. +// -------------------------------------------------------------- +define('DS', DIRECTORY_SEPARATOR); + +// -------------------------------------------------------------- +// The path to the application directory. +// -------------------------------------------------------------- +define('APP_PATH', realpath('application').'/'); + +// -------------------------------------------------------------- +// The path to the bundles directory. +// -------------------------------------------------------------- +define('BUNDLE_PATH', realpath('bundles').'/'); + +// -------------------------------------------------------------- +// The path to the storage directory. +// -------------------------------------------------------------- +define('STORAGE_PATH', realpath('storage').'/'); + +// -------------------------------------------------------------- +// The path to the Laravel directory. +// -------------------------------------------------------------- +define('SYS_PATH', realpath('laravel').'/'); + +// -------------------------------------------------------------- +// The path to the public directory. +// -------------------------------------------------------------- +define('PUBLIC_PATH', realpath('public').'/'); + +// -------------------------------------------------------------- +// Bootstrap the Laravel core. +// -------------------------------------------------------------- +require SYS_PATH.'core.php'; + +// -------------------------------------------------------------- +// Launch the Laravel "Artisan" CLI. +// -------------------------------------------------------------- +require SYS_PATH.'cli/artisan'.EXT; \ No newline at end of file diff --git a/bundles/.gitignore b/bundles/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/changelog.md b/changelog.md index f4abe742..9779ef6c 100644 --- a/changelog.md +++ b/changelog.md @@ -1,30 +1,5 @@ # Laravel Change Log -## Version 2.1.0 - -- Fix: Multiple wildcards / regular expressions per segment are now supported. - -### Upgrading from 2.0.9 - -- Replace **laravel** directory. - -## Version 2.0.9 - -- Minor: Made "timestamps" method in Eloquent model protected instead of private. -- Fix: Authentication cookies are not deleted properly when custom domains or paths are used. - -### Upgrading from 2.0.8 - -- Replace **laravel** directory. - -## Version 2.0.8 - -- Fix: Limited URI segments to 20 to protect against DDoS. - -### Upgrading from 2.0.7 - -- Replace **laravel** directory. - ## Version 2.0.7 - Fix: Fixed raw_where in query builder. diff --git a/laravel/arr.php b/laravel/arr.php deleted file mode 100644 index 966a6252..00000000 --- a/laravel/arr.php +++ /dev/null @@ -1,166 +0,0 @@ - - * // Get the $array['user']['name'] value from the array - * $name = Arr::get($array, 'user.name'); - * - * // Return a default from if the specified item doesn't exist - * $name = Arr::get($array, 'user.name', 'Taylor'); - * - * - * @param array $array - * @param string $key - * @param mixed $default - * @return mixed - */ - public static function get($array, $key, $default = null) - { - if (is_null($key)) return $array; - - foreach (explode('.', $key) as $segment) - { - if ( ! is_array($array) or ! array_key_exists($segment, $array)) - { - return ($default instanceof Closure) ? call_user_func($default) : $default; - } - - $array = $array[$segment]; - } - - return $array; - } - - /** - * Set an array item to a given value. - * - * The same "dot" syntax used by the "get" method may be used here. - * - * If no key is given to the method, the entire array will be replaced. - * - * - * // Set the $array['user']['name'] value on the array - * Arr::set($array, 'user.name', 'Taylor'); - * - * - * @param array $array - * @param string $key - * @param mixed $value - * @return void - */ - public static function set(&$array, $key, $value) - { - if (is_null($key)) return $array = $value; - - $keys = explode('.', $key); - - while (count($keys) > 1) - { - $key = array_shift($keys); - - if ( ! isset($array[$key]) or ! is_array($array[$key])) - { - $array[$key] = array(); - } - - $array =& $array[$key]; - } - - $array[array_shift($keys)] = $value; - } - - /** - * Remove an array item from a given array. - * - * The same "dot" syntax used by the "get" method may be used here. - * - * - * // Remove the $array['user']['name'] item from the array - * Arr::forget($array, 'user.name'); - * - * - * @param array $array - * @param string $key - * @return void - */ - public static function forget(&$array, $key) - { - if (is_null($key)) return; - - $keys = explode('.', $key); - - while (count($keys) > 1) - { - $key = array_shift($keys); - - if ( ! isset($array[$key]) or ! is_array($array[$key])) - { - return; - } - - $array =& $array[$key]; - } - - unset($array[array_shift($keys)]); - } - - /** - * Return the first element in an array which passes a given truth test. - * - * - * // Return the first array element that equals "Taylor" - * $value = Arr::first($array, function($k, $v) {return $v === 'Taylor';}); - * - * // Return a default value if no matching element is found - * $value = Arr::first($array, function($k, $v) {return $v === 'Taylor'}, 'Default'); - * - * - * @param array $array - * @param Closure $callback - * @param mixed $default - * @return mixed - */ - public static function first($array, $callback, $default = null) - { - foreach ($array as $key => $value) - { - if (call_user_func($callback, $key, $value)) return $value; - } - - return ($default instanceof Closure) ? call_user_func($default) : $default; - } - - /** - * Remove all array values that are contained within a given array of values. - * - * - * // Remove all array values that are empty strings - * $array = Arr::without($array, ''); - * - * // Remove all array values that are "One", "Two", or "Three" - * $array = Arr::without($array, array('One', 'Two', 'Three')); - * - * - * @param array $array - * @param array $without - * @return array - */ - public static function without($array, $without = array()) - { - $without = (array) $without; - - foreach ((array) $array as $key => $value) - { - if (in_array($value, $without)) unset($array[$key]); - } - - return $array; - } - -} \ No newline at end of file diff --git a/laravel/asset.php b/laravel/asset.php index 5b90a66a..f278143f 100644 --- a/laravel/asset.php +++ b/laravel/asset.php @@ -1,4 +1,4 @@ - * // Call the "styles" method on the default container @@ -60,6 +60,13 @@ class Asset_Container { */ public $name; + /** + * The bundle that the assets belong to. + * + * @var string + */ + public $bundle = DEFAULT_BUNDLE; + /** * All of the registered assets. * @@ -83,8 +90,8 @@ public function __construct($name) * Add an asset to the container. * * The extension of the asset source will be used to determine the type of - * asset being registered (CSS or JavaScript). If you are using a non-standard - * extension, you may use the style or script methods to register assets. + * asset being registered (CSS or JavaScript). When using a non-standard + * extension, the style/script methods may be used to register assets. * * * // Add an asset to the container @@ -107,7 +114,7 @@ public function add($name, $source, $dependencies = array(), $attributes = array { $type = (pathinfo($source, PATHINFO_EXTENSION) == 'css') ? 'style' : 'script'; - return call_user_func(array($this, $type), $name, $source, $dependencies, $attributes); + return $this->$type($name, $source, $dependencies, $attributes); } /** @@ -147,6 +154,29 @@ public function script($name, $source, $dependencies = array(), $attributes = ar return $this; } + /** + * Returns the full-path for an asset. + * + * @param string $source + * @return string + */ + public function path($source) + { + return Bundle::assets($this->bundle).$source; + } + + /** + * Set the bundle that the container's assets belong to. + * + * @param string $bundle + * @return Asset_Container + */ + public function bundle($bundle) + { + $this->bundle = $bundle; + return $this; + } + /** * Add an asset to the array of registered assets. * @@ -219,6 +249,14 @@ protected function asset($group, $name) $asset = $this->assets[$group][$name]; + // If the bundle source is not a complete URL, we will go ahead and prepend + // the bundle's asset path to the source provided with the asset. This will + // ensure that we attach the correct path to the asset. + if (filter_var($asset['source'], FILTER_VALIDATE_URL) === false) + { + $asset['source'] = Bundle::assets($this->bundle).$asset['source']; + } + return HTML::$group($asset['source'], $asset['attributes']); } @@ -257,7 +295,7 @@ protected function evaluate_asset($asset, $value, $original, &$sorted, &$assets) { // If the asset has no more dependencies, we can add it to the sorted list // and remove it from the array of assets. Otherwise, we will not verify - // the asset's dependencies and determine if they have already been sorted. + // the asset's dependencies and determine if they've been sorted. if (count($assets[$asset]['dependencies']) == 0) { $sorted[$asset] = $value; @@ -289,7 +327,8 @@ protected function evaluate_asset($asset, $value, $original, &$sorted, &$assets) * Verify that an asset's dependency is valid. * * A dependency is considered valid if it exists, is not a circular reference, and is - * not a reference to the owning asset itself. + * not a reference to the owning asset itself. If the dependency doesn't exist, no + * error or warning will be given. For the other cases, an exception is thrown. * * @param string $asset * @param string $dependency @@ -299,16 +338,20 @@ protected function evaluate_asset($asset, $value, $original, &$sorted, &$assets) */ protected function dependency_is_valid($asset, $dependency, $original, $assets) { - if ( ! isset($original[$dependency])) return false; - - if ($dependency === $asset) + if ( ! isset($original[$dependency])) { - throw new \LogicException("Asset [$asset] is dependent on itself."); + return false; + } + elseif ($dependency === $asset) + { + throw new \Exception("Asset [$asset] is dependent on itself."); } elseif (isset($assets[$dependency]) and in_array($asset, $assets[$dependency]['dependencies'])) { - throw new \LogicException("Assets [$asset] and [$dependency] have a circular dependency."); + throw new \Exception("Assets [$asset] and [$dependency] have a circular dependency."); } + + return true; } } diff --git a/laravel/auth.php b/laravel/auth.php index da5bd080..a43aa689 100644 --- a/laravel/auth.php +++ b/laravel/auth.php @@ -7,7 +7,7 @@ class Auth { * * @var object */ - protected static $user; + public static $user; /** * The key used when storing the user ID in the session. @@ -48,12 +48,6 @@ public static function check() /** * Get the current user of the application. * - * This method will call the "user" closure in the auth configuration file. - * If the user is not authenticated, null will be returned by the methd. - * - * If no user exists in the session, the method will check for a "remember me" - * cookie and attempt to login the user based on the value of that cookie. - * * * // Get the current user of the application * $user = Auth::user(); @@ -62,20 +56,26 @@ public static function check() * $email = Auth::user()->email; * * - * @return object + * @return object|null */ public static function user() { if ( ! is_null(static::$user)) return static::$user; - $id = IoC::core('session')->get(Auth::user_key); + $id = Session::get(Auth::user_key); - static::$user = call_user_func(Config::get('auth.user'), $id); + // To retrieve the user, we'll first attempt to use the "user" Closure + // defined in the auth configuration file, passing in the ID. The user + // Closure gives the developer a ton of freedom surrounding how the + // user is actually retrieved. + $config = Config::get('auth'); - // If the user was not found in the database, but a "remember me" cookie - // exists, we will attempt to recall the user based on the cookie value. - // Since all cookies contain a fingerprint hash verifying that the have - // not been modified on the client, we should be able to trust it. + static::$user = call_user_func($config['user'], $id); + + // If the user wasn't found in the database but a "remember me" cookie + // exists, we'll attempt to recall the user based on the cookie value. + // Since all cookies contain a fingerprint hash verifying that they + // haven't changed, we can trust it. $recaller = Cookie::get(Auth::remember_key); if (is_null(static::$user) and ! is_null($recaller)) @@ -94,13 +94,17 @@ public static function user() */ protected static function recall($recaller) { - // When the "remember me" cookie is stored, it is encrypted and contains the - // user's ID and a long, random string. The ID and string are separated by - // a pipe character. Since we exploded the decrypted string, we can just - // pass the first item in the array to the user Closure. + // When the remember me cookie is stored, it is encrypted and contains + // the user's ID and a long, random string. The segments are separated + // by a pipe character so we'll explode on that. $recaller = explode('|', Crypter::decrypt($recaller)); - if ( ! is_null($user = call_user_func(Config::get('auth.user'), $recaller[0]))) + // We'll pass the ID that was stored in the cookie into the same user + // Closure that is used by the "user" method. If the method returns + // a user, we will log them into the application. + $user = call_user_func(Config::get('auth.user'), $recaller[0]); + + if ( ! is_null($user)) { static::login($user); @@ -111,12 +115,13 @@ protected static function recall($recaller) /** * Attempt to log a user into the application. * - * If the credentials are valid, the user will be logged into the application - * and their user ID will be stored in the session via the "login" method. + * + * // Attempt to log a user into the application + * $success = Auth::attempt('username', 'password'); * - * The user may also be "remembered", which will keep the user logged into the - * application for one year or until they logout. The user is remembered via - * an encrypted cookie. + * // Attempt to login a user and set the "remember me" cookie + * Auth::attempt('username', 'password', true); + * * * @param string $username * @param string $password @@ -127,32 +132,35 @@ public static function attempt($username, $password = null, $remember = false) { $config = Config::get('auth'); - $user = call_user_func($config['attempt'], $username, $password, $config); + // When attempting to login the user, we will call the "attempt" closure + // from the configuration file. This gives the developer the freedom to + // authenticate based on the needs of their application. + // + // All of the password hashing and checking and left totally up to the + // developer, as this gives them the freedom to use any hashing scheme + // or authentication provider they wish. + $user = call_user_func($config['attempt'], $username, $password); - if ( ! is_null($user)) - { - static::login($user, $remember); + // If the user credentials were authenticated by the closure, we will + // log the user into the application, which will store their user ID + // in the session for subsequent requests. + if (is_null($user)) return false; - return true; - } + static::login($user, $remember); - return false; + return true; } /** * Log a user into the application. * - * An object representing the user or an integer user ID may be given to the method. - * If an object is given, the object must have an "id" property containing the user - * ID as it is stored in the database. - * * - * // Login a user by passing a user object - * Auth::login($user); - * * // Login the user with an ID of 15 * Auth::login(15); * + * // Login a user by passing a user object + * Auth::login($user); + * * // Login a user and set a "remember me" cookie * Auth::login($user, true); * @@ -167,11 +175,11 @@ public static function login($user, $remember = false) if ($remember) static::remember($id); - IoC::core('session')->put(Auth::user_key, $id); + Session::put(Auth::user_key, $id); } /** - * Set a cookie so that users are "remembered" and don't need to login. + * Set a cookie so that the user is "remembered". * * @param string $id * @return void @@ -183,7 +191,7 @@ protected static function remember($id) // This method assumes the "remember me" cookie should have the same // configuration as the session cookie. Since this cookie, like the // session cookie, should be kept very secure, it's probably safe - // to assume the settings are the same. + // to assume the settings are the same for this cookie. $config = Config::get('session'); extract($config, EXTR_SKIP); @@ -194,14 +202,13 @@ protected static function remember($id) /** * Log the current user out of the application. * - * The "logout" closure in the authenciation configuration file will be - * called. All authentication cookies will be deleted and the user ID - * will be removed from the session. - * * @return void */ public static function logout() { + // We will call the "logout" closure first, which gives the developer + // the chance to do any clean-up or before the user is logged out of + // the application. No action is taken by default. call_user_func(Config::get('auth.logout'), static::user()); static::$user = null; @@ -213,11 +220,9 @@ public static function logout() // When forgetting the cookie, we need to also pass in the path and // domain that would have been used when the cookie was originally // set by the framework, otherwise it will not be deleted. - Cookie::forget(Auth::user_key, $path, $domain, $secure); - Cookie::forget(Auth::remember_key, $path, $domain, $secure); - IoC::core('session')->forget(Auth::user_key); + Session::forget(Auth::user_key); } } \ No newline at end of file diff --git a/laravel/autoloader.php b/laravel/autoloader.php index 89603b72..982865ed 100644 --- a/laravel/autoloader.php +++ b/laravel/autoloader.php @@ -1,4 +1,4 @@ - - * // Returns "user/profile"... - * $file = static::controller('User_Profile_Controller'); - * - * - * @param string $class - * @return string - */ - protected static function controller($class) - { - $controller = str_replace(array('_', '_Controller'), array('/', ''), $class); - - return CONTROLLER_PATH.strtolower($controller).EXT; + // Once we have formatted the class name, we will simply spin + // through the registered PSR-0 directories and attempt to + // locate and load the class into the script. + foreach (static::$psr as $directory) + { + if (file_exists($path = $directory.strtolower($file).EXT)) + { + return require $path; + } + elseif (file_exists($path = $directory.$file.EXT)) + { + return require $path; + } + } } /** * Register an array of class to path mappings. * - * The mappings will be used to resolve file paths from class names when - * a class is lazy loaded through the Autoloader, providing a faster way - * of resolving file paths than the typical file_exists method. - * * * // Register a class mapping with the Autoloader - * Autoloader::maps(array('User' => MODEL_PATH.'user'.EXT)); + * Autoloader::map(array('User' => APP_PATH.'models/user.php')); * * * @param array $mappings * @return void */ - public static function maps($mappings) + public static function map($mappings) { - foreach ($mappings as $class => $path) - { - static::$mappings[$class] = $path; - } + static::$mappings = array_merge(static::$mappings, $mappings); } /** - * Register PSR-0 libraries with the Autoloader. + * Register a class alias with the auto-loader. * - * The library names given to this method should match directories within - * the application libraries directory. This method provides an easy way - * to indicate that some libraries should be loaded using the PSR-0 - * naming conventions instead of the Laravel conventions. - * - * - * // Register the "Assetic" library with the Autoloader - * Autoloader::libraries('Assetic'); - * - * // Register several libraries with the Autoloader - * Autoloader::libraries(array('Assetic', 'Twig')); - * - * - * @param array $libraries + * @param string $class + * @param string $alias * @return void */ - public static function libraries($libraries) + public static function alias($class, $alias) { - static::$libraries = array_merge(static::$libraries, (array) $libraries); + static::$aliases[$alias] = $class; + } + + /** + * Register directories to be searched as a PSR-0 library. + * + * @param string|array $directory + * @return void + */ + public static function psr($directory) + { + $directories = array_map(function($directory) + { + return rtrim($directory, '/').'/'; + + }, (array) $directory); + + static::$psr = array_unique(array_merge(static::$psr, $directories)); } } \ No newline at end of file diff --git a/laravel/benchmark.php b/laravel/benchmark.php deleted file mode 100644 index a0d29f3c..00000000 --- a/laravel/benchmark.php +++ /dev/null @@ -1,49 +0,0 @@ - + * // Returns the bundle path for the "admin" bundle + * $path = Bundle::path('admin'); + * + * // Returns the APP_PATH constant as the default bundle + * $path = Bundle::path('application'); + * + * + * @param string $bundle + * @return string + */ + public static function path($bundle) + { + return ($bundle != DEFAULT_BUNDLE) ? BUNDLE_PATH.strtolower($bundle).DS : APP_PATH; + } + + /** + * Return the root asset path for the given bundle. + * + * @param string $bundle + * @return string + */ + public static function assets($bundle) + { + return ($bundle != DEFAULT_BUNDLE) ? PUBLIC_PATH."bundles/{$bundle}/" : PUBLIC_PATH; + } + + /** + * Get the bundle name from a given identifier. + * + * + * // Returns "admin" as the bundle name for the identifier + * $bundle = Bundle::name('admin::home.index'); + * + * + * @param string $identifier + * @return string + */ + public static function name($identifier) + { + list($bundle, $element) = static::parse($identifier); + + return $bundle; + } + + /** + * Get the element name from a given identifier. + * + * + * // Returns "home.index" as the element name for the identifier + * $bundle = Bundle::bundle('admin::home.index'); + * + * + * @param string $identifier + * @return string + */ + public static function element($identifier) + { + list($bundle, $element) = static::parse($identifier); + + return $element; + } + + /** + * Reconstruct an identifier from a given bundle and element. + * + * + * // Returns "admin::home.index" + * $identifier = Bundle::identifier('admin', 'home.index'); + * + * // Returns "home.index" + * $identifier = Bundle::identifier('application', 'home.index'); + * + * + * @param string $bundle + * @param string $element + * @return string + */ + public static function identifier($bundle, $element) + { + return (is_null($bundle) or $bundle == DEFAULT_BUNDLE) ? $element : $bundle.'::'.$element; + } + + /** + * Return the bundle name if it exists, else return the default bundle. + * + * @param string $bundle + * @return string + */ + public static function resolve($bundle) + { + return (static::exists($bundle)) ? $bundle : DEFAULT_BUNDLE; + } + + /** + * Parse a element identifier and return the bundle name and element. + * + * + * // Returns array(null, 'admin.user') + * $element = Bundle::parse('admin.user'); + * + * // Parses "admin::user" and returns array('admin', 'user') + * $element = Bundle::parse('admin::user'); + * + * + * @param string $identifier + * @return array + */ + public static function parse($identifier) + { + // The parsed elements are cached so we don't have to reparse them on each + // subsequent request for the parsed element. So, if we've already parsed + // the given element, we'll just return the cached copy. + if (isset(static::$elements[$identifier])) + { + return static::$elements[$identifier]; + } + + if (strpos($identifier, '::') !== false) + { + $element = explode('::', strtolower($identifier)); + } + // If no bundle is in the identifier, we will insert the default bundle + // since classes like Config and Lang organize their items by bundle. + // The "application" folder essentially behaves as a bundle. + else + { + $element = array(DEFAULT_BUNDLE, strtolower($identifier)); + } + + return static::$elements[$identifier] = $element; + } + + /** + * Detect all of the existing bundles in the application. + * + * The names of the bundles are cached so this operation will be only be + * performed once and then the same array will be returned on each later + * request for the bundle names. + * + * @return array + */ + public static function all() + { + if (is_array(static::$bundles)) return static::$bundles; + + $bundles = array(); + + foreach (array_filter(glob(BUNDLE_PATH.'*'), 'is_dir') as $bundle) + { + $bundles[] = basename($bundle); + } + + return static::$bundles = $bundles; + } + +} \ No newline at end of file diff --git a/laravel/cache/manager.php b/laravel/cache.php similarity index 52% rename from laravel/cache/manager.php rename to laravel/cache.php index a216a2ea..cc044373 100644 --- a/laravel/cache/manager.php +++ b/laravel/cache.php @@ -1,23 +1,18 @@ - * // Get the default cache driver instance @@ -34,9 +29,9 @@ public static function driver($driver = null) { if (is_null($driver)) $driver = Config::get('cache.driver'); - if ( ! array_key_exists($driver, static::$drivers)) + if ( ! isset(static::$drivers[$driver])) { - return static::$drivers[$driver] = static::factory($driver); + static::$drivers[$driver] = static::factory($driver); } return static::$drivers[$driver]; @@ -53,33 +48,33 @@ protected static function factory($driver) switch ($driver) { case 'apc': - return new Drivers\APC(Config::get('cache.key')); + return new Cache\Drivers\APC(Config::get('cache.key')); case 'file': - return new Drivers\File(CACHE_PATH); + return new Cache\Drivers\File(CACHE_PATH); case 'memcached': - return new Drivers\Memcached(Memcached::instance(), Config::get('cache.key')); + return new Cache\Drivers\Memcached(Memcached::connection(), Config::get('cache.key')); case 'redis': - return new Drivers\Redis(Redis::db()); + return new Cache\Drivers\Redis(Redis::db()); + + case 'database': + return new Cache\Drivers\Database(Config::get('cache.key')); default: - throw new \DomainException("Cache driver {$driver} is not supported."); + throw new \Exception("Cache driver {$driver} is not supported."); } } /** - * Pass all other methods to the default cache driver. - * - * Passing method calls to the driver instance provides a convenient API - * for the developer when always using the default cache driver. + * Magic Method for calling the methods on the default cache driver. * * - * // Call the "get" method on the default driver + * // Call the "get" method on the default cache driver * $name = Cache::get('name'); * - * // Call the "put" method on the default driver + * // Call the "put" method on the default cache driver * Cache::put('name', 'Taylor', 15); * */ diff --git a/laravel/cache/drivers/database.php b/laravel/cache/drivers/database.php new file mode 100644 index 00000000..85e8b13d --- /dev/null +++ b/laravel/cache/drivers/database.php @@ -0,0 +1,113 @@ +key = $key; + } + + /** + * Determine if an item exists in the cache. + * + * @param string $key + * @return bool + */ + public function has($key) + { + return ( ! is_null($this->get($key))); + } + + /** + * Retrieve an item from the cache driver. + * + * @param string $key + * @return mixed + */ + protected function retrieve($key) + { + $cache = $this->table()->where('key', '=', $this->key.$key)->first(); + + if ( ! is_null($cache)) + { + if (time() >= $cache->expiration) return $this->forget($key); + + return unserialize($cache->value); + } + } + + /** + * Write an item to the cache for a given number of minutes. + * + * + * // Put an item in the cache for 15 minutes + * Cache::put('name', 'Taylor', 15); + * + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return void + */ + public function put($key, $value, $minutes) + { + $key = $this->key.$key; + + $value = serialize($value); + + $expiration = $this->expiration($minutes); + + // To update the value, we'll first attempt an insert against the + // database and if we catch an exception, we'll assume that the + // primary key already exists in the table and update. + try + { + $this->table()->insert(compact('key', 'value', 'expiration')); + } + catch (\Exception $e) + { + $this->table()->where('key', '=', $key)->update(compact('value', 'expiration')); + } + } + + /** + * Delete an item from the cache. + * + * @param string $key + * @return void + */ + public function forget($key) + { + $this->table()->where('key', '=', $this->key.$key)->delete(); + } + + /** + * Get a query builder for the database table. + * + * @return Query + */ + protected function table() + { + $connection = DB::connection(Config::get('cache.database.connection')); + + return $connection->table(Config::get('cache.database.table')); + } + +} \ No newline at end of file diff --git a/laravel/cache/drivers/driver.php b/laravel/cache/drivers/driver.php index a0241c5e..96bf0ef2 100644 --- a/laravel/cache/drivers/driver.php +++ b/laravel/cache/drivers/driver.php @@ -28,9 +28,7 @@ abstract public function has($key); */ public function get($key, $default = null) { - if ( ! is_null($item = $this->retrieve($key))) return $item; - - return ($default instanceof Closure) ? call_user_func($default) : $default; + return ( ! is_null($item = $this->retrieve($key))) ? $item : value($default); } /** @@ -57,8 +55,7 @@ abstract protected function retrieve($key); abstract public function put($key, $value, $minutes); /** - * Get an item from the cache. If the item doesn't exist in the - * cache, store the default value in the cache and return it. + * Get an item from the cache, or cache and return the default value. * * * // Get an item from the cache, or cache a value for 15 minutes @@ -77,9 +74,7 @@ public function remember($key, $default, $minutes) { if ( ! is_null($item = $this->get($key, null))) return $item; - $default = ($default instanceof Closure) ? call_user_func($default) : $default; - - $this->put($key, $default, $minutes); + $this->put($key, value($default), $minutes); return $default; } @@ -92,4 +87,15 @@ public function remember($key, $default, $minutes) */ abstract public function forget($key); + /** + * Get the expiration time as a UNIX timestamp. + * + * @param int $minutes + * @return int + */ + protected function expiration($minutes) + { + return time() + ($minutes * 60); + } + } diff --git a/laravel/cache/drivers/file.php b/laravel/cache/drivers/file.php index 6bf7c6e0..dbf4520f 100644 --- a/laravel/cache/drivers/file.php +++ b/laravel/cache/drivers/file.php @@ -68,7 +68,7 @@ protected function retrieve($key) */ public function put($key, $value, $minutes) { - $value = (time() + ($minutes * 60)).serialize($value); + $value = $this->expiration($minutes).serialize($value); file_put_contents($this->path.$key, $value, LOCK_EX); } @@ -81,10 +81,7 @@ public function put($key, $value, $minutes) */ public function forget($key) { - if (file_exists($this->path.$key)) - { - @unlink($this->path.$key); - } + if (file_exists($this->path.$key)) @unlink($this->path.$key); } } \ No newline at end of file diff --git a/laravel/cli/artisan.php b/laravel/cli/artisan.php new file mode 100644 index 00000000..b81355e6 --- /dev/null +++ b/laravel/cli/artisan.php @@ -0,0 +1,62 @@ +getMessage(); +} + +echo PHP_EOL; \ No newline at end of file diff --git a/laravel/cli/command.php b/laravel/cli/command.php new file mode 100644 index 00000000..09f9c5a8 --- /dev/null +++ b/laravel/cli/command.php @@ -0,0 +1,109 @@ +$method(array_slice($arguments, 1)); + } + + /** + * Parse the task name to extract the bundle, task, and method. + * + * @param string $task + * @return array + */ + protected static function parse($task) + { + list($bundle, $task) = Bundle::parse($task); + + // Extract the task method from the task string. Methods are called + // on tasks by separating the task and method with a single colon. + // If no task is specified, "run" is used as the default method. + if (str_contains($task, ':')) + { + list($task, $method) = explode(':', $task); + } + else + { + $method = 'run'; + } + + return array($bundle, $task, $method); + } + + /** + * Resolve an instance of the given task name. + * + * @param string $bundle + * @param string $task + * @return object + */ + public static function resolve($bundle, $task) + { + $identifier = Bundle::identifier($bundle, $task); + + // First we'll check to see if the task has been registered in + // the application IoC container. This allows dependencies to + // be injected into tasks for more testability. + if (IoC::registered("task: {$identifier}")) + { + return IoC::resolve("task: {$identifier}"); + } + + // If the task file exists, we'll format the bundle and task + // name into a task class name and resolve an instance of + // the so that the requested method may be executed. + if (file_exists($path = Bundle::path($bundle).'tasks/'.$task.EXT)) + { + require $path; + + $task = static::format($bundle, $task); + + return new $task; + } + } + + /** + * Format a bundle and task into a task class name. + * + * @param string $bundle + * @param string $task + * @return string + */ + protected static function format($bundle, $task) + { + $prefix = Bundle::class_prefix($bundle); + + return '\\'.$prefix.Str::clasify($task).'_Task'; + } + +} \ No newline at end of file diff --git a/laravel/cli/tasks/bundle/bundler.php b/laravel/cli/tasks/bundle/bundler.php new file mode 100644 index 00000000..67a209e8 --- /dev/null +++ b/laravel/cli/tasks/bundle/bundler.php @@ -0,0 +1,127 @@ +get($bundles) as $bundle) + { + if (is_dir(BUNDLE_PATH.$bundle['name'])) + { + echo "Bundle {$bundle['name']} is already installed."; + + continue; + } + + // Once we have the bundle information, we can resolve an instance + // of a provider and install the bundle into the application and + // all of its registered dependencies as well. + // + // Each bundle provider implements the Provider interface and + // is repsonsible for retrieving the bundle source from its + // hosting party and installing it into the application. + $provider = "bundle.provider: {$bundle['provider']}"; + + IoC::resolve($provider)->install($bundle); + + $publisher->publish($bundle); + } + } + + /** + * Publish bundle assets to the public directory. + * + * @param array $bundles + * @return void + */ + public function publish($bundles) + { + // If no bundles are passed to the command, we'll just gather all + // of the installed bundle names and publish the assets for each + // for each one of the bundles to the public directory. + if (count($bundles) == 0) $bundles = Bundle::all(); + + $publisher = IoC::resolve('bundle.publisher'); + + foreach ($bundles as $bundle) + { + $publisher->publish($bundle); + } + } + + /** + * Gather all of the bundles from the bundle repository. + * + * @param array $bundles + * @return array + */ + protected function get($bundles) + { + $responses = array(); + + $repository = IoC::resolve('bundle.repository'); + + // This method is primarily responsible for gathering the data + // for all bundles that need to be installed. This allows us + // to verify the existence of the bundle before even getting + // started on the actual installation process. + foreach ($bundles as $bundle) + { + // First we'll call the bundle repository to gather the bundle data + // array, which contains all of the information needed to install + // the bundle into the application. We'll verify that the bundle + // exists and the API is responding for each bundle. + $response = $repository->get($bundle); + + if ( ! $response) + { + throw new \Exception("The bundle API is not responding."); + } + + if ($response['status'] == 'not-found') + { + throw new \Exception("There is not a bundle named [$bundle]."); + } + + // If the bundle was retrieved successfully, we will add it to + // our array of bundles, as well as merge all of the bundle's + // dependencies into the array of responses so that they are + // installed along with the consuming dependency. + $bundle = $response['bundle']; + + $responses[] = $bundle; + + $responses = array_merge($responses, $this->get($bundle['dependencies'])); + } + + return $responses; + } + +} \ No newline at end of file diff --git a/laravel/cli/tasks/bundle/providers/github.php b/laravel/cli/tasks/bundle/providers/github.php new file mode 100644 index 00000000..174c9916 --- /dev/null +++ b/laravel/cli/tasks/bundle/providers/github.php @@ -0,0 +1,27 @@ +move($bundle, $this->from($bundle), $this->to($bundle)); + + echo "Assets published for bundle [$bundle].".PHP_EOL; + } + + /** + * Copy the contents of a bundle's assets to the public folder. + * + * @param string $bundle + * @param string $source + * @param string $destination + * @return void + */ + protected function move($bundle, $source, $destination) + { + if ( ! is_dir($source)) return; + + // First we need to create the destination directory if it doesn't + // already exists. This directory hosts all of the assets we copy + // from the installed bundle's source directory. + if ( ! is_dir($destination)) + { + mkdir($destination); + } + + $items = new FilesystemIterator($source, FilesystemIterator::SKIP_DOTS); + + foreach ($items as $item) + { + // If the file system item is a directory, we will recurse the + // function, passing in the item directory. To get the proper + // destination path, we'll replace the root bundle asset + // directory with the root public asset directory. + if ($item->isDir()) + { + $path = $item->getRealPath(); + + $recurse = str_replace($this->from($bundle), $this->to($bundle), $path); + + $this->move($bundle, $path, $recurse); + } + // If the file system item is an actual file, we can copy the + // file from the bundle asset directory to the public asset + // directory. The "copy" method will overwrite any existing + // files with the same name. + else + { + copy($item->getRealPath(), $destination.DS.$item->getBasename()); + } + } + } + + /** + * Get the "to" location of the bundle's assets. + * + * @param string $bundle + * @return string + */ + protected function to($bundle) + { + return PUBLIC_PATH.'bundles'.DS.$bundle.DS; + } + + /** + * Get the "from" location of the bundle's assets. + * + * @param string $bundle + * @return string + */ + protected function from($bundle) + { + return Bundle::path($bundle).'public'; + } + +} \ No newline at end of file diff --git a/laravel/cli/tasks/bundle/repository.php b/laravel/cli/tasks/bundle/repository.php new file mode 100644 index 00000000..2ee43d9c --- /dev/null +++ b/laravel/cli/tasks/bundle/repository.php @@ -0,0 +1,29 @@ +api.$bundle); + + return json_decode($bundle, true); + } + +} \ No newline at end of file diff --git a/laravel/cli/tasks/migrate/database.php b/laravel/cli/tasks/migrate/database.php new file mode 100644 index 00000000..e42f2713 --- /dev/null +++ b/laravel/cli/tasks/migrate/database.php @@ -0,0 +1,83 @@ +table()->insert(compact('bundle', 'name', 'batch')); + } + + /** + * Delete a row from the migration table. + * + * @param string $bundle + * @param string $name + * @return void + */ + public function delete($bundle, $name) + { + $this->table()->where_bundle_and_name($bundle, $name)->delete(); + } + + /** + * Return an array of the last batch of migrations. + * + * @return array + */ + public function last() + { + $table = $this->table(); + + // First we need to grab the last batch ID from the migration table, + // as this will allow us to grab the lastest batch of migrations + // that need to be run for a rollback command. + $id = $this->batch(); + + // Once we have the batch ID, we will pull all of the rows for that + // batch. Then we can feed the results into the resolve method to + // get the migration instances for the command. + return $table->where_batch($id)->order_by('name', 'desc')->get(); + } + + /** + * Get all of the migrations that have run for a bundle. + * + * @param string $bundle + * @return array + */ + public function ran($bundle) + { + return $this->table()->where_bundle($bundle)->lists('name'); + } + + /** + * Get the maximum batch ID from the migration table. + * + * @return int + */ + public function batch() + { + return $this->table()->max('batch'); + } + + /** + * Get a database query instance for the migration table. + * + * @return Query + */ + protected function table() + { + return DB::connection()->table('laravel_migrations'); + } + +} \ No newline at end of file diff --git a/laravel/cli/tasks/migrate/migrator.php b/laravel/cli/tasks/migrate/migrator.php new file mode 100644 index 00000000..dbd16f41 --- /dev/null +++ b/laravel/cli/tasks/migrate/migrator.php @@ -0,0 +1,235 @@ +resolver = $resolver; + $this->database = $database; + } + + /** + * Run a database migration command. + * + * @param array $arguments + * @return void + */ + public function run($arguments = array()) + { + // If no arguments were passed to the task, we will just migrate + // to the latest version across all bundles. Otherwise, we will + // parse the arguments to determine the bundle for which the + // database migrations should be run. + if (count($arguments) == 0) + { + $this->migrate(); + } + else + { + $this->migrate(array_get($arguments, 0)); + } + } + + /** + * Run the outstanding migrations for a given bundle. + * + * @param string $bundle + * @param int $version + * @return void + */ + public function migrate($bundle = null, $version = null) + { + $migrations = $this->resolver->outstanding($bundle); + + if (count($migrations) == 0) + { + echo "No outstanding migrations."; + + return; + } + + // We need to grab the latest batch ID and increment it + // by one. This allows us to group the migrations such + // that we can easily determine which migrations need + // to be rolled back for a given command. + $batch = $this->database->batch() + 1; + + foreach ($migrations as $migration) + { + $migration['migration']->up(); + + echo 'Migrated: '.$this->display($migration).PHP_EOL; + + // After running a migration, we log its execution in the + // migration table so that we can easily determine which + // migrations we will need to reverse on a rollback. + $this->database->log($migration['bundle'], $migration['name'], $batch); + } + } + + /** + * Rollback the latest migration command. + * + * @param array $arguments + * @return bool + */ + public function rollback($arguments = array()) + { + $migrations = $this->resolver->last(); + + if (count($migrations) == 0) + { + echo "Nothing to rollback."; + + return false; + } + + // The "last" method on the resolver returns an array of migrations, + // along with their bundles and names. We will iterate through each + // migration and run the "down" method, removing them from the + // database as we go. + foreach ($migrations as $migration) + { + $migration['migration']->down(); + + echo 'Rolled back: '.$this->display($migration).PHP_EOL; + + // By only removing the migration after it has successfully rolled back, + // we can re-run the rollback command in the event of any errors with + // the migration. When we re-run, only the migrations that have not + // been rolled-back for the batch will still be in the database. + $this->database->delete($migration['bundle'], $migration['name']); + } + + return true; + } + + /** + * Rollback all of the executed migrations. + * + * @param array $arguments + * @return void + */ + public function reset($arguments = array()) + { + while ($this->rollback()) {}; + } + + /** + * Install the database tables used by the migration system. + * + * @return void + */ + public function install() + { + Schema::table('laravel_migrations', function($table) + { + $table->create(); + + // Migrations can be run for a specific bundle, so we'll use + // the bundle name and string migration name as an unique ID + // for the migrations, allowing us to easily identify which + // migrations have been run for each bundle. + $table->string('bundle'); + + $table->string('name'); + + // When running a migration command, we will store a batch + // ID with each of the rows on the table. This will allow + // us to grab all of the migrations that were run for the + // last command when performing rollbacks. + $table->integer('batch'); + + $table->primary(array('bundle', 'name')); + }); + + echo "Migration table created successfully."; + } + + /** + * Generate a new migration file. + * + * @param array $arguments + * @return void + */ + public function make($arguments = array()) + { + if (count($arguments) == 0) + { + throw new \Exception("I need to know what to name the migration."); + } + + list($bundle, $migration) = Bundle::parse($arguments[0]); + + // The migration path is prefixed with the UNIX timestamp, which + // is a better way of ordering migrations than a simple integer + // incrementation, since developers may start working on the + // next migration at the same time unknowingly. + $date = date('Y_m_d').'_'.time(); + + $path = Bundle::path($bundle).'migrations/'.$date.'_'.$migration.EXT; + + File::put($path, $this->stub($bundle, $migration)); + + echo "Great! New migration created!"; + } + + /** + * Get the stub migration with the proper class name. + * + * @param string $bundle + * @param string $migration + * @return string + */ + protected function stub($bundle, $migration) + { + $stub = File::get(SYS_PATH.'cli/tasks/migrate/stub'.EXT); + + // The class name is formatted simialrly to tasks and controllers, + // where the bundle name is prefixed to the class if it is not in + // the default bundle. However, unlike tasks, there is nothing + // appended to the class name since they're already unique. + $class = Bundle::class_prefix($bundle).Str::classify($migration); + + return str_replace('{{class}}', $class, $stub); + } + + /** + * Get the migration bundle and name for display. + * + * @param array $migration + * @return string + */ + protected function display($migration) + { + return $migration['bundle'].'/'.$migration['name']; + } + +} \ No newline at end of file diff --git a/laravel/cli/tasks/migrate/resolver.php b/laravel/cli/tasks/migrate/resolver.php new file mode 100644 index 00000000..b6bf5064 --- /dev/null +++ b/laravel/cli/tasks/migrate/resolver.php @@ -0,0 +1,159 @@ +database = $database; + } + + /** + * Resolve all of the outstanding migrations for a bundle. + * + * @param string $bundle + * @return array + */ + public function outstanding($bundle = null) + { + $migrations = array(); + + // If no bundle was given to the command, we'll grab every bundle for + // the application, including the "application" bundle, which is not + // returned by "all" method on the Bundle class. + if (is_null($bundle)) + { + $bundles = array_merge(Bundle::all(), array('application')); + } + else + { + $bundles = array($bundle); + } + + foreach ($bundles as $bundle) + { + // First we need to grab all of the migrations that have already + // run for this bundle, as well as all of the migration files + // for the bundle. Once we have these, we can determine which + // migrations are still outstanding. + $ran = $this->database->ran($bundle); + + $files = $this->migrations($bundle); + + // To find outstanding migrations, we will simply iterate over + // the migration files and add the files that do not exist in + // the array of ran migrations to the outstanding array. + foreach ($files as $key => $name) + { + if ( ! in_array($name, $ran)) + { + $migrations[] = compact('bundle', 'name'); + } + } + } + + return $this->resolve($migrations); + } + + /** + * Resolve an array of the last batch of migrations. + * + * @return array + */ + public function last() + { + return $this->resolve($this->database->last()); + } + + /** + * Resolve an array of migration instances. + * + * @param array $migrations + * @return array + */ + protected function resolve($migrations) + { + $instances = array(); + + foreach ($migrations as $migration) + { + $migration = (array) $migration; + + // The migration array contains the bundle name, so we will get the + // path to the bundle's migrations and resolve an instance of the + // migration using the name. + $bundle = $migration['bundle']; + + $path = Bundle::path($bundle).'migrations/'; + + // Migrations are not resolved through the auto-loader, so we will + // manually instantiate the migration class instances for each of + // the migration names we're given. + $name = $migration['name']; + + require_once $path.$name.EXT; + + // Since the migration name will begin with the numeric ID, we'll + // slice off the ID so we are left with the migration class name. + // The IDs are for sorting when resolving outstanding migrations. + // + // Migrations that exist within bundles other than the default + // will be prefixed with the bundle name to avoid any possible + // naming collisions with other bundle's migrations. + $prefix = Bundle::class_prefix($bundle); + + $class = $prefix.substr($name, 22); + + $migration = new $class; + + // When adding to the array of instances, we will actually + // add the migration instance, the bundle, and the name. + // This allows the migrator to log the bundle and name + // when the migration is executed. + $instances[] = compact('bundle', 'name', 'migration'); + } + + return $instances; + } + + /** + * Grab all of the migration filenames for a bundle. + * + * @param string $bundle + * @return array + */ + protected function migrations($bundle) + { + $files = glob(Bundle::path($bundle).'migrations/*_*'.EXT); + + // Once we have the array of files in the migration directory, + // we'll take the basename of the file and remove the PHP file + // extension, which isn't needed. + foreach ($files as &$file) + { + $file = str_replace(EXT, '', basename($file)); + } + + // We'll also sort the files so that the earlier migrations + // will be at the front of the array and will be resolved + // first by this class' resolve method. + sort($files); + + return $files; + } + +} \ No newline at end of file diff --git a/laravel/cli/tasks/migrate/stub.php b/laravel/cli/tasks/migrate/stub.php new file mode 100644 index 00000000..6ce685f4 --- /dev/null +++ b/laravel/cli/tasks/migrate/stub.php @@ -0,0 +1,25 @@ + * @@ -46,29 +48,38 @@ public static function has($key) * // Get the "session" configuration array * $session = Config::get('session'); * + * // Get a configuration item from a bundle's configuration file + * $name = Config::get('admin::names.first'); + * * // Get the "timezone" option from the "application" configuration file * $timezone = Config::get('application.timezone'); * * * @param string $key - * @param string $default * @return array */ - public static function get($key, $default = null) + public static function get($key) { - list($file, $key) = static::parse($key); - - if ( ! static::load($file)) + // First, we'll check the keyed cache of configuration items, as this will + // be the fastest method of retrieving the configuration option. After an + // item is retrieved, it is always stored in the cache by its key. + if (array_key_exists($key, static::$cache)) { - return ($default instanceof Closure) ? call_user_func($default) : $default; + return static::$cache[$key]; } - $items = static::$items[$file]; + list($bundle, $file, $item) = static::parse($key); + + if ( ! static::load($bundle, $file)) return; + + $items = static::$items[$bundle][$file]; // If a specific configuration item was not requested, the key will be null, // meaning we need to return the entire array of configuration item from the // requested configuration file. Otherwise we can return the item. - return (is_null($key)) ? $items : Arr::get($items, $key, $default); + $value = (is_null($item)) ? $items : array_get($items, $item); + + return static::$cache[$key] = $value; } /** @@ -78,6 +89,9 @@ public static function get($key, $default = null) * // Set the "session" configuration array * Config::set('session', $array); * + * // Set a configuration option that belongs by a bundle + * Config::set('admin::names.first', 'Taylor'); + * * // Set the "timezone" option in the "application" configuration file * Config::set('application.timezone', 'UTC'); * @@ -88,61 +102,53 @@ public static function get($key, $default = null) */ public static function set($key, $value) { - list($file, $key) = static::parse($key); - - static::load($file); - - if (is_null($key)) - { - Arr::set(static::$items, $file, $value); - } - else - { - Arr::set(static::$items[$file], $key, $value); - } + static::$cache[$key] = $value; } /** - * Parse a configuration key and return its file and key segments. + * Parse a key and return its bundle, file, and key segments. * - * The first segment of a configuration key represents the configuration - * file, while the remaining segments represent an item within that file. - * If no item segment is present, null will be returned for the item value - * indicating that the entire configuration array should be returned. + * Configuration items are named using the {bundle}::{file}.{item} convention. * * @param string $key * @return array */ protected static function parse($key) { - $segments = explode('.', $key); + $bundle = Bundle::name($key); + $segments = explode('.', Bundle::element($key)); + + // If there are not at least two segments in the array, it means that the + // developer is requesting the entire configuration array to be returned. + // If that is the case, we'll make the item field of the array "null". if (count($segments) >= 2) { - return array($segments[0], implode('.', array_slice($segments, 1))); + return array($bundle, $segments[0], implode('.', array_slice($segments, 1))); } else { - return array($segments[0], null); + return array($bundle, $segments[0], null); } } /** * Load all of the configuration items from a configuration file. * + * @param string $bundle * @param string $file * @return bool */ - public static function load($file) + public static function load($bundle, $file) { - if (isset(static::$items[$file])) return true; + if (isset(static::$items[$bundle][$file])) return true; $config = array(); - // Configuration files cascade. Typically, the system configuration array is - // loaded first, followed by the application array, providing the convenient - // cascading of configuration options from system to application. - foreach (static::$paths as $directory) + // Configuration files cascade. Typically, the bundle configuration array is + // loaded first, followed by the environment array, providing the convenient + // cascading of configuration options across environments. + foreach (static::paths($bundle) as $directory) { if ($directory !== '' and file_exists($path = $directory.$file.EXT)) { @@ -150,9 +156,37 @@ public static function load($file) } } - if (count($config) > 0) static::$items[$file] = $config; + if (count($config) > 0) + { + static::$items[$bundle][$file] = $config; + } - return isset(static::$items[$file]); + return isset(static::$items[$bundle][$file]); + } + + /** + * Get the array of configuration paths that should be searched for a bundle. + * + * @param string $bundle + * @return array + */ + protected static function paths($bundle) + { + $paths[] = Bundle::path($bundle).'config/'; + + // Configuration files can be made specific for a given environment. If an + // environment has been set, we will merge the environment configuration + // in last, so that it overrides all other options. + // + // This allows the developer to quickly and easily create configurations + // for various scenarios, such as local development and production, + // without constantly changing configuration files. + if (isset($_SERVER['LARAVEL_ENV'])) + { + $paths[] = $paths[count($paths) - 1].$_SERVER['LARAVEL_ENV'].'/'; + } + + return $paths; } } \ No newline at end of file diff --git a/laravel/config/ascii.php b/laravel/config/ascii.php deleted file mode 100644 index 37b795bf..00000000 --- a/laravel/config/ascii.php +++ /dev/null @@ -1,73 +0,0 @@ - 'ae', - '/œ/' => 'oe', - '/À|Á|Â|Ã|Ä|Å|Ǻ|Ā|Ă|Ą|Ǎ|А/' => 'A', - '/à|á|â|ã|ä|å|ǻ|ā|ă|ą|ǎ|ª|а/' => 'a', - '/Б/' => 'B', - '/б/' => 'b', - '/Ç|Ć|Ĉ|Ċ|Č|Ц/' => 'C', - '/ç|ć|ĉ|ċ|č|ц/' => 'c', - '/Ð|Ď|Đ|Д/' => 'Dj', - '/ð|ď|đ|д/' => 'dj', - '/È|É|Ê|Ë|Ē|Ĕ|Ė|Ę|Ě|Е|Ё|Э/' => 'E', - '/è|é|ê|ë|ē|ĕ|ė|ę|ě|е|ё|э/' => 'e', - '/Ф/' => 'F', - '/ƒ|ф/' => 'f', - '/Ĝ|Ğ|Ġ|Ģ|Г/' => 'G', - '/ĝ|ğ|ġ|ģ|г/' => 'g', - '/Ĥ|Ħ|Х/' => 'H', - '/ĥ|ħ|х/' => 'h', - '/Ì|Í|Î|Ï|Ĩ|Ī|Ĭ|Ǐ|Į|İ|И/' => 'I', - '/ì|í|î|ï|ĩ|ī|ĭ|ǐ|į|ı|и/' => 'i', - '/Ĵ|Й/' => 'J', - '/ĵ|й/' => 'j', - '/Ķ|К/' => 'K', - '/ķ|к/' => 'k', - '/Ĺ|Ļ|Ľ|Ŀ|Ł|Л/' => 'L', - '/ĺ|ļ|ľ|ŀ|ł|л/' => 'l', - '/М/' => 'M', - '/м/' => 'm', - '/Ñ|Ń|Ņ|Ň|Н/' => 'N', - '/ñ|ń|ņ|ň|ʼn|н/' => 'n', - '/Ö|Ò|Ó|Ô|Õ|Ō|Ŏ|Ǒ|Ő|Ơ|Ø|Ǿ|О/' => 'O', - '/ö|ò|ó|ô|õ|ō|ŏ|ǒ|ő|ơ|ø|ǿ|º|о/' => 'o', - '/П/' => 'P', - '/п/' => 'p', - '/Ŕ|Ŗ|Ř|Р/' => 'R', - '/ŕ|ŗ|ř|р/' => 'r', - '/Ś|Ŝ|Ş|Š|С/' => 'S', - '/ś|ŝ|ş|š|ſ|с/' => 's', - '/Ţ|Ť|Ŧ|Т/' => 'T', - '/ţ|ť|ŧ|т/' => 't', - '/Ù|Ú|Û|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ|У/' => 'U', - '/ù|ú|û|ũ|ū|ŭ|ů|ű|ų|ư|ǔ|ǖ|ǘ|ǚ|ǜ|у/' => 'u', - '/В/' => 'V', - '/в/' => 'v', - '/Ý|Ÿ|Ŷ|Ы/' => 'Y', - '/ý|ÿ|ŷ|ы/' => 'y', - '/Ŵ/' => 'W', - '/ŵ/' => 'w', - '/Ź|Ż|Ž|З/' => 'Z', - '/ź|ż|ž|з/' => 'z', - '/Æ|Ǽ/' => 'AE', - '/ß/'=> 'ss', - '/IJ/' => 'IJ', - '/ij/' => 'ij', - '/Œ/' => 'OE', - '/Ч/' => 'Ch', - '/ч/' => 'ch', - '/Ю/' => 'Ju', - '/ю/' => 'ju', - '/Я/' => 'Ja', - '/я/' => 'ja', - '/Ш/' => 'Sh', - '/ш/' => 'sh', - '/Щ/' => 'Shch', - '/щ/' => 'shch', - '/Ж/' => 'Zh', - '/ж/' => 'zh', - -); \ No newline at end of file diff --git a/laravel/cookie.php b/laravel/cookie.php index f478174c..bcc2c643 100644 --- a/laravel/cookie.php +++ b/laravel/cookie.php @@ -1,8 +1,10 @@ - + * // Get the value of the "favorite" cookie + * $favorite = Cookie::get('favorite'); + * + * // Get the value of a cookie or return a default value if it doesn't exist + * $favorite = Cookie::get('framework', 'Laravel'); + * + * * @param string $name * @param mixed $default * @return string */ public static function get($name, $default = null) { - $value = Arr::get($_COOKIE, $name); + $value = array_get($_COOKIE, $name); - if ( ! is_null($value)) + if ( ! is_null($value) and isset($value[40]) and $value[40] == '~') { - // All Laravel managed cookies are "signed" with a fingerprint hash. - // The hash serves to verify that the contents of the cookie have not - // been modified by the user. We can verify the integrity of the cookie - // by extracting the value and re-hashing it, then comparing that hash - // against the hash stored in the cookie. - if (isset($value[40]) and $value[40] === '~') - { - list($hash, $value) = explode('~', $value, 2); + // The hash signature and the cookie value are separated by a tilde + // character for convenience. To separate the hash and the contents + // we can simply expode on that character. + // + // By re-feeding the cookie value into the "sign" method, we should + // be able to generate a hash that matches the one taken out of the + // cookie. If they don't match, the cookie value has been changed. + list($hash, $value) = explode('~', $value, 2); - if (static::hash($name, $value) === $hash) - { - return $value; - } + if (static::hash($name, $value) === $hash) + { + return $value; } } - return ($default instanceof Closure) ? call_user_func($default) : $default; + return value($default); } /** - * Set a "permanent" cookie. The cookie will last for one year. + * Set the value of a cookie. * - * @param string $name - * @param string $value - * @param string $path - * @param string $domain - * @param bool $secure - * @param bool $http_only - * @return bool - */ - public static function forever($name, $value, $path = '/', $domain = null, $secure = false, $http_only = false) - { - return static::put($name, $value, 525600, $path, $domain, $secure, $http_only); - } - - /** - * Set the value of a cookie. + * If the response headers have already been sent, the cookie will not be set. * - * If a negative number of minutes is specified, the cookie will be deleted. + * + * // Set the value of the "favorite" cookie + * Cookie::put('favorite', 'Laravel'); * - * This method's signature is very similar to the PHP setcookie method. - * However, you simply need to pass the number of minutes for which you - * wish the cookie to be valid. No funky time calculation is required. + * // Set the value of the "favorite" cookie for twenty minutes + * Cookie::put('favorite', 'Laravel', 20); + * * * @param string $name * @param string $value @@ -81,39 +78,51 @@ public static function forever($name, $value, $path = '/', $domain = null, $secu * @param string $path * @param string $domain * @param bool $secure - * @param bool $http_only * @return bool */ - public static function put($name, $value, $minutes = 0, $path = '/', $domain = null, $secure = false, $http_only = false) + public static function put($name, $value, $minutes = 0, $path = '/', $domain = null, $secure = false) { if (headers_sent()) return false; $time = ($minutes !== 0) ? time() + ($minutes * 60) : 0; - $value = static::hash($name, $value).'~'.$value; - - if ($minutes < 0) - { - unset($_COOKIE[$name]); - } - else - { - $_COOKIE[$name] = $value; - } - - return setcookie($name, $value, $time, $path, $domain, $secure, $http_only); + return setcookie($name, static::sign($name, $value), $time, $path, $domain, $secure); } /** - * Generate a cookie hash. + * Set a "permanent" cookie. The cookie will last for one year. * - * Cookie salts are used to verify that the contents of the cookie have not - * been modified by the user, since they serve as a fingerprint of the cookie - * contents. The application key is used to salt the salts. + * + * // Set a cookie that should last one year + * Cookie::forever('favorite', 'Blue'); + * * - * When the cookie is read using the "get" method, the value will be extracted - * from the cookie and hashed, if the hash in the cookie and the hashed value - * do not match, we know the cookie has been changed on the client. + * @param string $name + * @param string $value + * @param string $path + * @param string $domain + * @param bool $secure + * @return bool + */ + public static function forever($name, $value, $path = '/', $domain = null, $secure = false) + { + return static::put($name, $value, 525600, $path, $domain, $secure); + } + + /** + * Generate a cookie signature based on the contents. + * + * @param string $name + * @param string $value + * @return string + */ + protected static function sign($name, $value) + { + return static::hash($name, $value).'~'.$value; + } + + /** + * Generate a cookie hash based on the contents. * * @param string $name * @param string $value @@ -121,7 +130,7 @@ public static function put($name, $value, $minutes = 0, $path = '/', $domain = n */ protected static function hash($name, $value) { - return sha1($name.$value.Config::$items['application']['key']); + return sha1($name.$value.Config::get('application.key')); } /** @@ -131,12 +140,11 @@ protected static function hash($name, $value) * @param string $path * @param string $domain * @param bool $secure - * @param bool $http_only * @return bool */ - public static function forget($name, $path = '/', $domain = null, $secure = false, $http_only = false) + public static function forget($name, $path = '/', $domain = null, $secure = false) { - return static::put($name, null, -2000, $path, $domain, $secure, $http_only); + return static::put($name, null, -2000, $path, $domain, $secure); } } \ No newline at end of file diff --git a/laravel/core.php b/laravel/core.php index 066cfad6..6c799e7e 100644 --- a/laravel/core.php +++ b/laravel/core.php @@ -9,59 +9,22 @@ define('EXT', '.php'); define('CRLF', "\r\n"); define('BLADE_EXT', '.blade.php'); -define('APP_PATH', realpath($application).'/'); -define('PUBLIC_PATH', realpath($public).'/'); -define('SYS_PATH', realpath($laravel).'/'); -define('STORAGE_PATH', APP_PATH.'storage/'); -define('CACHE_PATH', STORAGE_PATH.'cache/'); -define('CONFIG_PATH', APP_PATH.'config/'); -define('CONTROLLER_PATH', APP_PATH.'controllers/'); -define('DATABASE_PATH', STORAGE_PATH.'database/'); -define('LANG_PATH', APP_PATH.'language/'); -define('LIBRARY_PATH', APP_PATH.'libraries/'); -define('MODEL_PATH', APP_PATH.'models/'); -define('ROUTE_PATH', APP_PATH.'routes/'); -define('SESSION_PATH', STORAGE_PATH.'sessions/'); -define('SYS_CONFIG_PATH', SYS_PATH.'config/'); -define('VIEW_PATH', APP_PATH.'views/'); - -/** - * Define the Laravel environment configuration path. This path is used - * by the configuration class to load configuration options specific for - * the server environment, allowing the developer to conveniently change - * configuration options based on the application environment. - * - */ -$environment = ''; - -if (isset($_SERVER['LARAVEL_ENV'])) -{ - $environment = CONFIG_PATH.$_SERVER['LARAVEL_ENV'].'/'; -} - -define('ENV_CONFIG_PATH', $environment); - -unset($application, $public, $laravel, $environment); +define('CACHE_PATH', STORAGE_PATH.'cache'.DS); +define('DATABASE_PATH', STORAGE_PATH.'database'.DS); +define('SESSION_PATH', STORAGE_PATH.'sessions'.DS); +define('DEFAULT_BUNDLE', 'application'); +define('MB_STRING', (int) function_exists('mb_get_info')); /** * Require all of the classes that can't be loaded by the auto-loader. * These are typically classes that the auto-loader itself relies upon * to load classes, such as the array and configuration classes. */ -require SYS_PATH.'arr'.EXT; +require SYS_PATH.'bundle'.EXT; require SYS_PATH.'config'.EXT; -require SYS_PATH.'facades'.EXT; +require SYS_PATH.'helpers'.EXT; require SYS_PATH.'autoloader'.EXT; -/** - * Load a few of the core configuration files that are loaded for every - * request to the application. It is quicker to load them manually each - * request rather than parse the keys for every request. - */ -Config::load('application'); -Config::load('session'); -Config::load('error'); - /** * Register the Autoloader's "load" method on the auto-loader stack. * This method provides the lazy-loading of all class files, as well @@ -75,22 +38,27 @@ * More mappings can also be registered by the developer as needed. */ Autoloader::$mappings = array( - 'Laravel\\Arr' => SYS_PATH.'arr'.EXT, - 'Laravel\\Asset' => SYS_PATH.'asset'.EXT, 'Laravel\\Auth' => SYS_PATH.'auth'.EXT, + 'Laravel\\Asset' => SYS_PATH.'asset'.EXT, 'Laravel\\Benchmark' => SYS_PATH.'benchmark'.EXT, 'Laravel\\Blade' => SYS_PATH.'blade'.EXT, + 'Laravel\\Bundle' => SYS_PATH.'bundle'.EXT, + 'Laravel\\Cache' => SYS_PATH.'cache'.EXT, 'Laravel\\Config' => SYS_PATH.'config'.EXT, 'Laravel\\Cookie' => SYS_PATH.'cookie'.EXT, 'Laravel\\Crypter' => SYS_PATH.'crypter'.EXT, + 'Laravel\\Database' => SYS_PATH.'database'.EXT, + 'Laravel\\Error' => SYS_PATH.'error'.EXT, + 'Laravel\\Event' => SYS_PATH.'event'.EXT, 'Laravel\\File' => SYS_PATH.'file'.EXT, + 'Laravel\\Fluent' => SYS_PATH.'fluent'.EXT, 'Laravel\\Form' => SYS_PATH.'form'.EXT, 'Laravel\\Hash' => SYS_PATH.'hash'.EXT, 'Laravel\\HTML' => SYS_PATH.'html'.EXT, - 'Laravel\\Inflector' => SYS_PATH.'inflector'.EXT, 'Laravel\\Input' => SYS_PATH.'input'.EXT, 'Laravel\\IoC' => SYS_PATH.'ioc'.EXT, 'Laravel\\Lang' => SYS_PATH.'lang'.EXT, + 'Laravel\\Log' => SYS_PATH.'log'.EXT, 'Laravel\\Memcached' => SYS_PATH.'memcached'.EXT, 'Laravel\\Messages' => SYS_PATH.'messages'.EXT, 'Laravel\\Paginator' => SYS_PATH.'paginator'.EXT, @@ -99,40 +67,62 @@ 'Laravel\\Request' => SYS_PATH.'request'.EXT, 'Laravel\\Response' => SYS_PATH.'response'.EXT, 'Laravel\\Section' => SYS_PATH.'section'.EXT, + 'Laravel\\Session' => SYS_PATH.'session'.EXT, 'Laravel\\Str' => SYS_PATH.'str'.EXT, 'Laravel\\URI' => SYS_PATH.'uri'.EXT, 'Laravel\\URL' => SYS_PATH.'url'.EXT, 'Laravel\\Validator' => SYS_PATH.'validator'.EXT, 'Laravel\\View' => SYS_PATH.'view'.EXT, - 'Laravel\\Cache\\Manager' => SYS_PATH.'cache/manager'.EXT, + 'Laravel\\Cache\\Drivers\\APC' => SYS_PATH.'cache/drivers/apc'.EXT, 'Laravel\\Cache\\Drivers\\Driver' => SYS_PATH.'cache/drivers/driver'.EXT, 'Laravel\\Cache\\Drivers\\File' => SYS_PATH.'cache/drivers/file'.EXT, 'Laravel\\Cache\\Drivers\\Memcached' => SYS_PATH.'cache/drivers/memcached'.EXT, 'Laravel\\Cache\\Drivers\\Redis' => SYS_PATH.'cache/drivers/redis'.EXT, + 'Laravel\\Cache\\Drivers\\Database' => SYS_PATH.'cache/drivers/database'.EXT, + + 'Laravel\\CLI\\Command' => SYS_PATH.'cli/command'.EXT, + 'Laravel\\CLI\\Tasks\\Task' => SYS_PATH.'cli/tasks/task'.EXT, + 'Laravel\\CLI\\Tasks\\Bundle\\Bundler' => SYS_PATH.'cli/tasks/bundle/bundler'.EXT, + 'Laravel\\CLI\\Tasks\\Bundle\\Repository' => SYS_PATH.'cli/tasks/bundle/repository'.EXT, + 'Laravel\\CLI\\Tasks\\Bundle\\Publisher' => SYS_PATH.'cli/tasks/bundle/publisher'.EXT, + 'Laravel\\CLI\\Tasks\\Bundle\\Providers\\Provider' => SYS_PATH.'cli/tasks/bundle/providers/provider'.EXT, + 'Laravel\\CLI\\Tasks\\Bundle\\Providers\\Github' => SYS_PATH.'cli/tasks/bundle/providers/github'.EXT, + 'Laravel\\CLI\\Tasks\\Migrate\\Migrator' => SYS_PATH.'cli/tasks/migrate/migrator'.EXT, + 'Laravel\\CLI\\Tasks\\Migrate\\Resolver' => SYS_PATH.'cli/tasks/migrate/resolver'.EXT, + 'Laravel\\CLI\\Tasks\\Migrate\\Database' => SYS_PATH.'cli/tasks/migrate/database'.EXT, + 'Laravel\\Database\\Connection' => SYS_PATH.'database/connection'.EXT, 'Laravel\\Database\\Expression' => SYS_PATH.'database/expression'.EXT, - 'Laravel\\Database\\Manager' => SYS_PATH.'database/manager'.EXT, 'Laravel\\Database\\Query' => SYS_PATH.'database/query'.EXT, + 'Laravel\\Database\\Schema' => SYS_PATH.'database/schema'.EXT, + 'Laravel\\Database\\Grammar' => SYS_PATH.'database/grammar'.EXT, 'Laravel\\Database\\Connectors\\Connector' => SYS_PATH.'database/connectors/connector'.EXT, 'Laravel\\Database\\Connectors\\MySQL' => SYS_PATH.'database/connectors/mysql'.EXT, 'Laravel\\Database\\Connectors\\Postgres' => SYS_PATH.'database/connectors/postgres'.EXT, 'Laravel\\Database\\Connectors\\SQLite' => SYS_PATH.'database/connectors/sqlite'.EXT, - 'Laravel\\Database\\Eloquent\\Hydrator' => SYS_PATH.'database/eloquent/hydrator'.EXT, - 'Laravel\\Database\\Eloquent\\Model' => SYS_PATH.'database/eloquent/model'.EXT, - 'Laravel\\Database\\Grammars\\Grammar' => SYS_PATH.'database/grammars/grammar'.EXT, - 'Laravel\\Database\\Grammars\\MySQL' => SYS_PATH.'database/grammars/mysql'.EXT, + 'Laravel\\Database\\Connectors\\SQLServer' => SYS_PATH.'database/connectors/sqlserver'.EXT, + 'Laravel\\Database\\Query\\Grammars\\Grammar' => SYS_PATH.'database/query/grammars/grammar'.EXT, + 'Laravel\\Database\\Query\\Grammars\\MySQL' => SYS_PATH.'database/query/grammars/mysql'.EXT, + 'Laravel\\Database\\Query\\Grammars\\SQLServer' => SYS_PATH.'database/query/grammars/sqlserver'.EXT, + 'Laravel\\Database\\Schema\\Table' => SYS_PATH.'database/schema/table'.EXT, + 'Laravel\\Database\\Schema\\Grammars\\Grammar' => SYS_PATH.'database/schema/grammars/grammar'.EXT, + 'Laravel\\Database\\Schema\\Grammars\\MySQL' => SYS_PATH.'database/schema/grammars/mysql'.EXT, + 'Laravel\\Database\\Schema\\Grammars\\Postgres' => SYS_PATH.'database/schema/grammars/postgres'.EXT, + 'Laravel\\Database\\Schema\\Grammars\\SQLServer' => SYS_PATH.'database/schema/grammars/sqlserver'.EXT, + 'Laravel\\Database\\Schema\\Grammars\\SQLite' => SYS_PATH.'database/schema/grammars/sqlite'.EXT, + 'Laravel\\Routing\\Controller' => SYS_PATH.'routing/controller'.EXT, 'Laravel\\Routing\\Filter' => SYS_PATH.'routing/filter'.EXT, - 'Laravel\\Routing\\Loader' => SYS_PATH.'routing/loader'.EXT, + 'Laravel\\Routing\\Filter_Collection' => SYS_PATH.'routing/filter'.EXT, 'Laravel\\Routing\\Route' => SYS_PATH.'routing/route'.EXT, 'Laravel\\Routing\\Router' => SYS_PATH.'routing/router'.EXT, + 'Laravel\\Session\\Payload' => SYS_PATH.'session/payload'.EXT, 'Laravel\\Session\\Drivers\\APC' => SYS_PATH.'session/drivers/apc'.EXT, 'Laravel\\Session\\Drivers\\Cookie' => SYS_PATH.'session/drivers/cookie'.EXT, 'Laravel\\Session\\Drivers\\Database' => SYS_PATH.'session/drivers/database'.EXT, 'Laravel\\Session\\Drivers\\Driver' => SYS_PATH.'session/drivers/driver'.EXT, - 'Laravel\\Session\\Drivers\\Factory' => SYS_PATH.'session/drivers/factory'.EXT, 'Laravel\\Session\\Drivers\\File' => SYS_PATH.'session/drivers/file'.EXT, 'Laravel\\Session\\Drivers\\Memcached' => SYS_PATH.'session/drivers/memcached'.EXT, 'Laravel\\Session\\Drivers\\Redis' => SYS_PATH.'session/drivers/redis'.EXT, @@ -140,16 +130,9 @@ ); /** - * Register the default timezone for the application. This will be - * the default timezone used by all date / timezone functions in - * the entire application. + * Register all of the core class aliases. These aliases provide a + * convenient way of working with the Laravel core classes without + * having to worry about the namespacing. The developer is also + * free to remove aliases when they extend core classes. */ -date_default_timezone_set(Config::$items['application']['timezone']); - -/** - * Define a few global, convenient functions. These functions - * provide short-cuts for things like the retrieval of language - * lines and HTML::entities. They just make our lives as devs a - * little sweeter and more enjoyable. - */ -require SYS_PATH.'helpers'.EXT; \ No newline at end of file +Autoloader::$aliases = Config::get('application.aliases'); \ No newline at end of file diff --git a/laravel/crypter.php b/laravel/crypter.php index c6e3e2e4..eb45a518 100644 --- a/laravel/crypter.php +++ b/laravel/crypter.php @@ -1,8 +1,8 @@ - - * // Encrypt a string using the Mcrypt PHP extension - * $encrypted = Crypter::encrpt('secret'); - * + * The string will be encrypted using the AES-256 scheme and will be base64 encoded. * * @param string $value * @return string @@ -50,26 +41,27 @@ public static function encrypt($value) /** * Decrypt a string using Mcrypt. * - * The given encrypted value must have been encrypted using Laravel and - * the application key specified in the application configuration file. - * - * Mcrypt must be installed on your machine before using this method. - * * @param string $value * @return string */ public static function decrypt($value) { - if (($value = base64_decode($value)) === false) - { - throw new \InvalidArgumentException('Input value is not valid base64 data.'); - } + $value = base64_decode($value); + // To decrypt the value, we first need to extract the input vector and + // the encrypted value. The input vector size varies across different + // encryption ciphers and modes, so we will get the correct size for + // the cipher and mode being used by the class. $iv = substr($value, 0, static::iv_size()); $value = substr($value, static::iv_size()); - return rtrim(mcrypt_decrypt(static::$cipher, static::key(), $value, static::$mode, $iv), "\0"); + // Once we have the input vector and the value, we can give them both + // to Mcrypt for decryption. The value is sometimes padded with \0, + // so we will trim all of the padding characters from the string. + $key = static::key(); + + return rtrim(mcrypt_decrypt(static::$cipher, $key, $value, static::$mode, $iv), "\0"); } /** @@ -89,7 +81,7 @@ protected static function iv_size() */ protected static function key() { - return Config::$items['application']['key']; + return Config::get('application.key'); } } \ No newline at end of file diff --git a/laravel/database/manager.php b/laravel/database.php similarity index 65% rename from laravel/database/manager.php rename to laravel/database.php index f24118e0..2595bc9e 100644 --- a/laravel/database/manager.php +++ b/laravel/database.php @@ -1,16 +1,16 @@ -connect($config); } /** * Create a new database connector instance. * - * The database connectors are responsible for simply establishing a PDO - * database connection given a configuration. This allows us to easily - * drop in support for new database systems by writing a connector. - * * @param string $driver * @return Connector */ @@ -83,16 +69,19 @@ protected static function connector($driver) switch ($driver) { case 'sqlite': - return new Connectors\SQLite(DATABASE_PATH); + return new Database\Connectors\SQLite; case 'mysql': - return new Connectors\MySQL; + return new Database\Connectors\MySQL; case 'pgsql': - return new Connectors\Postgres; + return new Database\Connectors\Postgres; + + case 'sqlsrv': + return new Database\Connectors\SQLServer; default: - throw new \DomainException("Database driver [$driver] is not supported."); + throw new \Exception("Database driver [$driver] is not supported."); } } @@ -124,7 +113,13 @@ public static function raw($value) /** * Magic Method for calling methods on the default database connection. * - * This provides a convenient API for querying or examining the default database connection. + * + * // Get the driver name for the default database connection + * $driver = DB::driver(); + * + * // Execute a fluent query on the default database connection + * $users = DB::table('users')->get(); + * */ public static function __callStatic($method, $parameters) { diff --git a/laravel/database/connection.php b/laravel/database/connection.php index 9147412a..c9dbad4d 100644 --- a/laravel/database/connection.php +++ b/laravel/database/connection.php @@ -9,13 +9,6 @@ class Connection { */ public $pdo; - /** - * All of the queries that have been executed on the connection. - * - * @var array - */ - public $queries = array(); - /** * The connection configuration array. * @@ -30,6 +23,13 @@ class Connection { */ protected $grammar; + /** + * All of the queries that have been executed on all connections. + * + * @var array + */ + public static $queries = array(); + /** * Create a new database connection instance. * @@ -46,6 +46,14 @@ public function __construct(PDO $pdo, $config) /** * Begin a fluent query against a table. * + * + * // Start a fluent query against the "users" table + * $query = DB::connection()->table('users'); + * + * // Start a fluent query against the "users" table and get all the users + * $users = DB::connection()->table('users')->get(); + * + * * @param string $table * @return Query */ @@ -57,27 +65,22 @@ public function table($table) /** * Create a new query grammar for the connection. * - * Query grammars allow support for new database systems to be added quickly - * and easily. Since the responsibility of the query generation is delegated - * to the grammar classes, it is simple to override only the methods with - * SQL syntax that differs from the default implementation. - * - * @return Grammars\Grammar + * @return Query\Grammars\Grammar */ protected function grammar() { if (isset($this->grammar)) return $this->grammar; - // We allow the developer to hard-code a grammar for the connection. This really - // has no use yet; however, if database systems that can use multiple grammars - // like ODBC are added in the future, this will be needed. switch (isset($this->config['grammar']) ? $this->config['grammar'] : $this->driver()) { case 'mysql': - return $this->grammar = new Grammars\MySQL; + return $this->grammar = new Query\Grammars\MySQL; + + case 'sqlsrv': + return $this->grammar = new Query\Grammars\SQLServer; default: - return $this->grammar = new Grammars\Grammar; + return $this->grammar = new Query\Grammars\Grammar; } } @@ -98,9 +101,9 @@ protected function grammar() */ public function only($sql, $bindings = array()) { - $result = (array) $this->first($sql, $bindings); + $results = (array) $this->first($sql, $bindings); - return reset($result); + return reset($results); } /** @@ -127,67 +130,124 @@ public function first($sql, $bindings = array()) } /** - * Execute a SQL query against the connection. - * - * The method returns the following based on query type: - * - * SELECT -> Array of stdClasses - * UPDATE -> Number of rows affected. - * DELETE -> Number of Rows affected. - * ELSE -> Boolean true / false depending on success. - * - * - * // Execute a query against the database connection - * $users = DB::connection()->query('select * from users'); - * - * // Execute a query with bound parameters - * $user = DB::connection()->query('select * from users where id = ?', array($id)); - * + * Execute a SQL query and return an array of StdClass objects. * * @param string $sql * @param array $bindings - * @return mixed + * @return array */ public function query($sql, $bindings = array()) { - // Since expressions are injected into the query as raw strings, we need - // to remove them from the array of bindings. They are not truly bound - // to the PDO statement as named parameters. - foreach ($bindings as $key => $value) - { - if ($value instanceof Expression) unset($bindings[$key]); - } + list($statement, $result) = $this->execute($sql, $bindings); - $bindings = array_values($bindings); + return $statement->fetchAll(PDO::FETCH_CLASS, 'stdClass'); + } + + /** + * Execute a SQL UPDATE query and return the affected row count. + * + * @param string $sql + * @param array $bindings + * @return int + */ + public function update($sql, $bindings = array()) + { + list($statement, $result) = $this->execute($sql, $bindings); + + return $statement->rowCount(); + } + + /** + * Execute a SQL DELETE query and return the affected row count. + * + * @param string $sql + * @param array $bindings + * @return int + */ + public function delete($sql, $bindings = array()) + { + list($statement, $result) = $this->execute($sql, $bindings); + + return $statement->rowCount(); + } + + /** + * Execute an SQL query and return the boolean result of the PDO statement. + * + * @param string $sql + * @param array $bindings + * @return bool + */ + public function statement($sql, $bindings = array()) + { + list($statement, $result) = $this->execute($sql, $bindings); + + return $result; + } + + /** + * Execute a SQL query against the connection. + * + * The PDO statement and boolean result will be return in an array. + * + * @param string $sql + * @param array $bindings + * @return array + */ + protected function execute($sql, $bindings = array()) + { + // Since expressions are injected into the query as strings, we need to + // remove them from the array of bindings. After we have removed them, + // we'll reset the array so there aren't gaps in the keys. + $bindings = array_values(array_filter($bindings, function($binding) + { + return ! $binding instanceof Expression; + })); $sql = $this->transform($sql, $bindings); - $this->queries[] = compact('sql', 'bindings'); + $statement = $this->pdo->prepare($sql); - return $this->execute($this->pdo->prepare($sql), $bindings); + // Every query is timed so that we can log the executinon time along + // with the query SQL and array of bindings. This should be make it + // convenient for the developer to profile the application's query + // performance to diagnose bottlenecks. + $time = microtime(true); + + $result = $statement->execute($bindings); + + $time = number_format((microtime(true) - $time) * 1000, 2); + + // Once we have execute the query, we log the SQL, bindings, and + // execution time in a static array that is accessed by all of + // the connections used by the application. This allows us to + // review all of the executed SQL. + static::$queries[] = compact('sql', 'bindings', 'time'); + + return array($statement, $result); } /** * Transform an SQL query into an executable query. * - * Laravel provides a convenient short-cut when writing raw queries for - * handling cumbersome "where in" statements. This method will transform - * those segments into their full SQL counterparts. - * * @param string $sql * @param array $bindings * @return string */ protected function transform($sql, $bindings) { + // Laravel provides an easy short-cut notation for writing raw + // WHERE IN statements. If (...) is in the query, it will be + // replaced with the correct number of parameters based on + // the bindings for the query. if (strpos($sql, '(...)') !== false) { for ($i = 0; $i < count($bindings); $i++) { - // If the binding is an array, we can assume it is being used to fill - // a "where in" condition, so we will replace the next place-holder - // in the query with the correct number of parameters based on the - // number of elements in this binding. + // If the binding is an array, we can assume it is being used + // to fill a "where in" condition, so we'll replace the next + // place-holder in the SQL query with the correct number of + // parameters based on the elements in the binding. if (is_array($bindings[$i])) { $parameters = implode(', ', array_fill(0, count($bindings[$i]), '?')); @@ -200,33 +260,6 @@ protected function transform($sql, $bindings) return trim($sql); } - /** - * Execute a prepared PDO statement and return the appropriate results. - * - * @param PDOStatement $statement - * @param array $bindings - * @return mixed - */ - protected function execute(PDOStatement $statement, $bindings) - { - $result = $statement->execute($bindings); - - $sql = strtoupper($statement->queryString); - - if (strpos($sql, 'SELECT') === 0) - { - return $statement->fetchAll(PDO::FETCH_CLASS, 'stdClass'); - } - elseif (strpos($sql, 'UPDATE') === 0 or strpos($sql, 'DELETE') === 0) - { - return $statement->rowCount(); - } - else - { - return $result; - } - } - /** * Get the driver name for the database connection. * diff --git a/laravel/database/connectors/connector.php b/laravel/database/connectors/connector.php index 3294bbc0..01065583 100644 --- a/laravel/database/connectors/connector.php +++ b/laravel/database/connectors/connector.php @@ -16,7 +16,7 @@ abstract class Connector { ); /** - * Establish a PDO database connection for a given database configuration. + * Establish a PDO database connection. * * @param array $config * @return PDO @@ -24,7 +24,7 @@ abstract class Connector { abstract public function connect($config); /** - * Get the PDO connection options for a given database configuration. + * Get the PDO connection options for the configuration. * * Developer specified options will override the default connection options. * diff --git a/laravel/database/connectors/mysql.php b/laravel/database/connectors/mysql.php index 260722f2..16cbc2a7 100644 --- a/laravel/database/connectors/mysql.php +++ b/laravel/database/connectors/mysql.php @@ -3,7 +3,7 @@ class MySQL extends Connector { /** - * Establish a PDO database connection for a given database configuration. + * Establish a PDO database connection. * * @param array $config * @return PDO @@ -15,7 +15,7 @@ public function connect($config) // Format the initial MySQL PDO connection string. These options are required // for every MySQL connection that is established. The connection strings // have the following convention: "mysql:host=hostname;dbname=database" - $dsn = sprintf('%s:host=%s;dbname=%s', $driver, $host, $database); + $dsn = "mysql:host={$host};dbname={$database}"; // Check for any optional MySQL PDO options. These options are not required // to establish a PDO connection; however, may be needed in certain server diff --git a/laravel/database/connectors/postgres.php b/laravel/database/connectors/postgres.php index f9c0e672..0ac9f832 100644 --- a/laravel/database/connectors/postgres.php +++ b/laravel/database/connectors/postgres.php @@ -3,7 +3,7 @@ class Postgres extends Connector { /** - * Establish a PDO database connection for a given database configuration. + * Establish a PDO database connection. * * @param array $config * @return PDO @@ -15,7 +15,7 @@ public function connect($config) // Format the initial Postgres PDO connection string. These options are required // for every Postgres connection that is established. The connection strings // have the following convention: "pgsql:host=hostname;dbname=database" - $dsn = sprintf('%s:host=%s;dbname=%s', $driver, $host, $database); + $dsn = "pgsql:host={$host};dbname={$database}"; // Check for any optional Postgres PDO options. These options are not required // to establish a PDO connection; however, may be needed in certain server diff --git a/laravel/database/connectors/sqlite.php b/laravel/database/connectors/sqlite.php index 0ffe8ed6..61f12b14 100644 --- a/laravel/database/connectors/sqlite.php +++ b/laravel/database/connectors/sqlite.php @@ -3,25 +3,7 @@ class SQLite extends Connector { /** - * The path to the SQLite databases for the application. - * - * @var string - */ - protected $path; - - /** - * Create a new SQLite database connector instance. - * - * @param string $path - * @return void - */ - public function __construct($path) - { - $this->path = $path; - } - - /** - * Establish a PDO database connection for a given database configuration. + * Establish a PDO database connection. * * @param array $config * @return PDO @@ -32,27 +14,19 @@ public function connect($config) // SQLite provides supported for "in-memory" databases, which exist only for the // lifetime of the request. Any given in-memory database may only have one PDO - // connection open to it at a time. Generally, these databases are use for + // connection open to it at a time. Generally, these databases are used for // testing and development purposes, not in production scenarios. if ($config['database'] == ':memory:') { return new PDO('sqlite::memory:', null, null, $options); } - // First, we will check for the database in the default storage directory for the - // application. If we don't find the database there, we will assume the database - // name is actually a full qualified path to the database on disk and attempt - // to load it. If we still can't find it, we'll bail out. - elseif (file_exists($path = $this->path.$config['database'].'.sqlite')) + if (file_exists($path = DATABASE_PATH.$config['database'].'.sqlite')) { return new PDO('sqlite:'.$path, null, null, $options); } - elseif (file_exists($config['database'])) - { - return new PDO('sqlite:'.$config['database'], null, null, $options); - } - throw new \OutOfBoundsException("SQLite database [{$config['database']}] could not be found."); + throw new \Exception("SQLite database [{$config['database']}] could not be found."); } } diff --git a/laravel/database/connectors/sqlserver.php b/laravel/database/connectors/sqlserver.php new file mode 100644 index 00000000..8e57b99c --- /dev/null +++ b/laravel/database/connectors/sqlserver.php @@ -0,0 +1,38 @@ + PDO::CASE_LOWER, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, + PDO::ATTR_STRINGIFY_FETCHES => false, + ); + + /** + * Establish a PDO database connection. + * + * @param array $config + * @return PDO + */ + public function connect($config) + { + extract($config); + + // Format the SQL Server connection string. This connection string format can + // also be used to connect to Azure SQL Server databases. The port is defined + // directly after the server name, so we'll create that and then create the + // final DSN string to pass to PDO. + $port = (isset($port)) ? ','.$port : ''; + + $dsn = "sqlsrv:Server={$host}{$port};Database={$database}"; + + return new PDO($dsn, $username, $password, $this->options($config)); + } + +} \ No newline at end of file diff --git a/laravel/database/eloquent/hydrator.php b/laravel/database/eloquent/hydrator.php deleted file mode 100644 index 7c188d63..00000000 --- a/laravel/database/eloquent/hydrator.php +++ /dev/null @@ -1,212 +0,0 @@ -query->get()); - - if (count($results) > 0) - { - foreach ($eloquent->includes as $include) - { - if ( ! method_exists($eloquent, $include)) - { - throw new \LogicException("Attempting to eager load [$include], but the relationship is not defined."); - } - - static::eagerly($eloquent, $results, $include); - } - } - - return $results; - } - - /** - * Hydrate the base models for a query. - * - * The resulting model array is keyed by the primary keys of the models. - * This allows the models to easily be matched to their children. - * - * @param string $class - * @param array $results - * @return array - */ - private static function base($class, $results) - { - $models = array(); - - foreach ($results as $result) - { - $model = new $class; - - $model->attributes = (array) $result; - - $model->exists = true; - - if (isset($model->attributes['id'])) - { - $models[$model->id] = $model; - } - else - { - $models[] = $model; - } - } - - return $models; - } - - /** - * Eagerly load a relationship. - * - * @param object $eloquent - * @param array $parents - * @param string $include - * @return void - */ - private static function eagerly($eloquent, &$parents, $include) - { - // We temporarily spoof the query attributes to allow the query to be fetched without - // any problems, since the belongs_to method actually gets the related attribute. - $first = reset($parents); - - $eloquent->attributes = $first->attributes; - - $relationship = $eloquent->$include(); - - $eloquent->attributes = array(); - - // Reset the WHERE clause and bindings on the query. We'll add our own WHERE clause soon. - // This will allow us to load a range of related models instead of only one. - $relationship->query->reset_where(); - - // Initialize the relationship attribute on the parents. As expected, "many" relationships - // are initialized to an array and "one" relationships are initialized to null. - foreach ($parents as &$parent) - { - $parent->ignore[$include] = (in_array($eloquent->relating, array('has_many', 'has_and_belongs_to_many'))) ? array() : null; - } - - if (in_array($relating = $eloquent->relating, array('has_one', 'has_many', 'belongs_to'))) - { - return static::$relating($relationship, $parents, $eloquent->relating_key, $include); - } - else - { - static::has_and_belongs_to_many($relationship, $parents, $eloquent->relating_key, $eloquent->relating_table, $include); - } - } - - /** - * Eagerly load a 1:1 relationship. - * - * @param object $relationship - * @param array $parents - * @param string $relating_key - * @param string $relating - * @param string $include - * @return void - */ - private static function has_one($relationship, &$parents, $relating_key, $include) - { - foreach ($relationship->where_in($relating_key, array_keys($parents))->get() as $key => $child) - { - $parents[$child->$relating_key]->ignore[$include] = $child; - } - } - - /** - * Eagerly load a 1:* relationship. - * - * @param object $relationship - * @param array $parents - * @param string $relating_key - * @param string $relating - * @param string $include - * @return void - */ - private static function has_many($relationship, &$parents, $relating_key, $include) - { - foreach ($relationship->where_in($relating_key, array_keys($parents))->get() as $key => $child) - { - $parents[$child->$relating_key]->ignore[$include][$child->id] = $child; - } - } - - /** - * Eagerly load a 1:1 belonging relationship. - * - * @param object $relationship - * @param array $parents - * @param string $relating_key - * @param string $include - * @return void - */ - private static function belongs_to($relationship, &$parents, $relating_key, $include) - { - $keys = array(); - - foreach ($parents as &$parent) - { - $keys[] = $parent->$relating_key; - } - - $children = $relationship->where_in('id', array_unique($keys))->get(); - - foreach ($parents as &$parent) - { - if (array_key_exists($parent->$relating_key, $children)) - { - $parent->ignore[$include] = $children[$parent->$relating_key]; - } - } - } - - /** - * Eagerly load a many-to-many relationship. - * - * @param object $relationship - * @param array $parents - * @param string $relating_key - * @param string $relating_table - * @param string $include - * - * @return void - */ - private static function has_and_belongs_to_many($relationship, &$parents, $relating_key, $relating_table, $include) - { - // The model "has and belongs to many" method sets the SELECT clause; however, we need - // to clear it here since we will be adding the foreign key to the select. - $relationship->query->select = null; - - $relationship->query->where_in($relating_table.'.'.$relating_key, array_keys($parents)); - - // The foreign key is added to the select to allow us to easily match the models back to their parents. - // Otherwise, there would be no apparent connection between the models to allow us to match them. - $children = $relationship->query->get(array(Model::table(get_class($relationship)).'.*', $relating_table.'.'.$relating_key)); - - $class = get_class($relationship); - - foreach ($children as $child) - { - $related = new $class; - - $related->attributes = (array) $child; - - $related->exists = true; - - // Remove the foreign key since it was only added to the query to help match the models. - unset($related->attributes[$relating_key]); - - $parents[$child->$relating_key]->ignore[$include][$child->id] = $related; - } - } - -} diff --git a/laravel/database/eloquent/model.php b/laravel/database/eloquent/model.php deleted file mode 100644 index 0699a730..00000000 --- a/laravel/database/eloquent/model.php +++ /dev/null @@ -1,538 +0,0 @@ -fill($attributes); - } - - /** - * Set the attributes of the model using an array. - * - * @param array $attributes - * @return Model - */ - public function fill($attributes) - { - foreach ($attributes as $key => $value) - { - $this->$key = $value; - } - - return $this; - } - - /** - * Set the eagerly loaded models on the queryable model. - * - * @return Model - */ - private function _with() - { - $this->includes = func_get_args(); - - return $this; - } - - /** - * Factory for creating queryable Eloquent model instances. - * - * @param string $class - * @return object - */ - public static function query($class) - { - $model = new $class; - - // Since this method is only used for instantiating models for querying - // purposes, we will go ahead and set the Query instance on the model. - $model->query = DB::connection(static::$connection)->table(static::table($class)); - - return $model; - } - - /** - * Get the table name for a model. - * - * @param string $class - * @return string - */ - public static function table($class) - { - if (property_exists($class, 'table')) return $class::$table; - - return strtolower(Inflector::plural(static::model_name($class))); - } - - /** - * Get an Eloquent model name without any namespaces. - * - * @param string|Model $model - * @return string - */ - public static function model_name($model) - { - $class = (is_object($model)) ? get_class($model) : $model; - - $segments = array_reverse(explode('\\', $class)); - - return $segments[0]; - } - - /** - * Get all of the models from the database. - * - * @return array - */ - public static function all() - { - return Hydrator::hydrate(static::query(get_called_class())); - } - - /** - * Get a model by the primary key. - * - * @param int $id - * @return mixed - */ - public static function find($id) - { - return static::query(get_called_class())->where('id', '=', $id)->first(); - } - - /** - * Get an array of models from the database. - * - * @return array - */ - private function _get() - { - return Hydrator::hydrate($this); - } - - /** - * Get the first model result - * - * @return mixed - */ - private function _first() - { - return (count($results = $this->take(1)->_get()) > 0) ? reset($results) : null; - } - - /** - * Get paginated model results as a Paginator instance. - * - * @param int $per_page - * @return Paginator - */ - private function _paginate($per_page = null) - { - $total = $this->query->count(); - - // The number of models to show per page may be specified as a static property - // on the model. The models shown per page may also be overriden for the model - // by passing the number into this method. If the models to show per page is - // not available via either of these avenues, a default number will be shown. - if (is_null($per_page)) - { - $per_page = (property_exists(get_class($this), 'per_page')) ? static::$per_page : 20; - } - - return Paginator::make($this->for_page(Paginator::page($total, $per_page), $per_page)->get(), $total, $per_page); - } - - /** - * Retrieve the query for a 1:1 relationship. - * - * @param string $model - * @param string $foreign_key - * @return mixed - */ - public function has_one($model, $foreign_key = null) - { - $this->relating = __FUNCTION__; - - return $this->has_one_or_many($model, $foreign_key); - } - - /** - * Retrieve the query for a 1:* relationship. - * - * @param string $model - * @param string $foreign_key - * @return mixed - */ - public function has_many($model, $foreign_key = null) - { - $this->relating = __FUNCTION__; - - return $this->has_one_or_many($model, $foreign_key); - } - - /** - * Retrieve the query for a 1:1 or 1:* relationship. - * - * The default foreign key for has one and has many relationships is the name - * of the model with an appended _id. For example, the foreign key for a - * User model would be user_id. Photo would be photo_id, etc. - * - * @param string $model - * @param string $foreign_key - * @return mixed - */ - private function has_one_or_many($model, $foreign_key) - { - $this->relating_key = (is_null($foreign_key)) ? strtolower(static::model_name($this)).'_id' : $foreign_key; - - return static::query($model)->where($this->relating_key, '=', $this->id); - } - - /** - * Retrieve the query for a 1:1 belonging relationship. - * - * The default foreign key for belonging relationships is the name of the - * relationship method name with _id. So, if a model has a "manager" method - * returning a belongs_to relationship, the key would be manager_id. - * - * @param string $model - * @param string $foreign_key - * @return mixed - */ - public function belongs_to($model, $foreign_key = null) - { - $this->relating = __FUNCTION__; - - if ( ! is_null($foreign_key)) - { - $this->relating_key = $foreign_key; - } - else - { - list(, $caller) = debug_backtrace(false); - - $this->relating_key = $caller['function'].'_id'; - } - - return static::query($model)->where('id', '=', $this->attributes[$this->relating_key]); - } - - /** - * Retrieve the query for a *:* relationship. - * - * The default foreign key for many-to-many relations is the name of the model - * with an appended _id. This is the same convention as has_one and has_many. - * - * @param string $model - * @param string $table - * @param string $foreign_key - * @param string $associated_key - * @return mixed - */ - public function has_and_belongs_to_many($model, $table = null, $foreign_key = null, $associated_key = null) - { - $this->relating = __FUNCTION__; - - $this->relating_table = (is_null($table)) ? $this->intermediate_table($model) : $table; - - // Allowing the overriding of the foreign and associated keys provides - // the flexibility for self-referential many-to-many relationships. - $this->relating_key = (is_null($foreign_key)) ? strtolower(static::model_name($this)).'_id' : $foreign_key; - - // The associated key is the foreign key name of the related model. - // If the related model is "Role", the key would be "role_id". - $associated_key = (is_null($associated_key)) ? strtolower(static::model_name($model)).'_id' : $associated_key; - - return static::query($model) - ->select(array(static::table($model).'.*')) - ->join($this->relating_table, static::table($model).'.id', '=', $this->relating_table.'.'.$associated_key) - ->where($this->relating_table.'.'.$this->relating_key, '=', $this->id); - } - - /** - * Determine the intermediate table name for a given model. - * - * By default, the intermediate table name is the plural names of the models - * arranged alphabetically and concatenated with an underscore. - * - * @param string $model - * @return string - */ - private function intermediate_table($model) - { - $models = array(Inflector::plural(static::model_name($model)), Inflector::plural(static::model_name($this))); - - sort($models); - - return strtolower($models[0].'_'.$models[1]); - } - - /** - * Save the model to the database. - * - * @return bool - */ - public function save() - { - // If the model does not have any dirty attributes, there is no reason - // to save it to the database. - if ($this->exists and count($this->dirty) == 0) return true; - - $model = get_class($this); - - // Since the model was instantiated using "new", a query instance has not been set. - // Only models being used for querying have their query instances set by default. - $this->query = DB::connection(static::$connection)->table(static::table($model)); - - if (property_exists($model, 'timestamps') and $model::$timestamps) - { - $this->timestamp(); - } - - // If the model already exists in the database, we will just update it. - // Otherwise, we will insert the model and set the ID attribute. - if ($this->exists) - { - $success = ($this->query->where_id($this->attributes['id'])->update($this->dirty) === 1); - } - else - { - $success = is_numeric($this->attributes['id'] = $this->query->insert_get_id($this->attributes, static::$sequence)); - } - - ($this->exists = true) and $this->dirty = array(); - - return $success; - } - - /** - * Set the creation and update timestamps on the model. - * - * @return void - */ - protected function timestamp() - { - $this->updated_at = date('Y-m-d H:i:s'); - - if ( ! $this->exists) $this->created_at = $this->updated_at; - } - - /** - * Delete a model from the database. - * - * @param int $id - * @return int - */ - public function delete($id = null) - { - // If the delete method is being called on an existing model, we only want to delete - // that model. If it is being called from an Eloquent query model, it is probably - // the developer's intention to delete more than one model, so we will pass the - // delete statement to the query instance. - if ( ! $this->exists) return $this->query->delete(); - - $table = static::table(get_class($this)); - - return DB::connection(static::$connection)->table($table)->delete($this->id); - } - - /** - * Magic method for retrieving model attributes. - */ - public function __get($key) - { - if (array_key_exists($key, $this->attributes)) - { - return $this->attributes[$key]; - } - // Is the requested item a model relationship that has already been loaded? - // All of the loaded relationships are stored in the "ignore" array. - elseif (array_key_exists($key, $this->ignore)) - { - return $this->ignore[$key]; - } - // Is the requested item a model relationship? If it is, we will dynamically - // load it and return the results of the relationship query. - elseif (method_exists($this, $key)) - { - $query = $this->$key(); - - return $this->ignore[$key] = (in_array($this->relating, array('has_one', 'belongs_to'))) ? $query->first() : $query->get(); - } - } - - /** - * Magic Method for setting model attributes. - */ - public function __set($key, $value) - { - // If the key is a relationship, add it to the ignored attributes. - // Ignored attributes are not stored in the database. - if (method_exists($this, $key)) - { - $this->ignore[$key] = $value; - } - else - { - $this->attributes[$key] = $value; - $this->dirty[$key] = $value; - } - } - - /** - * Magic Method for determining if a model attribute is set. - */ - public function __isset($key) - { - return (array_key_exists($key, $this->attributes) or array_key_exists($key, $this->ignore)); - } - - /** - * Magic Method for unsetting model attributes. - */ - public function __unset($key) - { - unset($this->attributes[$key], $this->ignore[$key], $this->dirty[$key]); - } - - /** - * Magic Method for handling dynamic method calls. - */ - public function __call($method, $parameters) - { - // To allow the "with", "get", "first", and "paginate" methods to be called both - // staticly and on an instance, we need to have private, underscored versions - // of the methods and handle them dynamically. - if (in_array($method, array('with', 'get', 'first', 'paginate'))) - { - return call_user_func_array(array($this, '_'.$method), $parameters); - } - - // All of the aggregate and persistance functions can be passed directly to the query - // instance. For these functions, we can simply return the response of the query. - if (in_array($method, array('insert', 'update', 'increment', 'decrement', 'abs', 'count', 'sum', 'min', 'max', 'avg'))) - { - return call_user_func_array(array($this->query, $method), $parameters); - } - - // Pass the method to the query instance. This allows the chaining of methods - // from the query builder, providing the same convenient query API as the - // query builder itself. - call_user_func_array(array($this->query, $method), $parameters); - - return $this; - } - - /** - * Magic Method for handling dynamic static method calls. - */ - public static function __callStatic($method, $parameters) - { - // Just pass the method to a model instance and let the __call method take care of it. - return call_user_func_array(array(static::query(get_called_class()), $method), $parameters); - } - -} \ No newline at end of file diff --git a/laravel/database/grammar.php b/laravel/database/grammar.php new file mode 100644 index 00000000..f98a1cf7 --- /dev/null +++ b/laravel/database/grammar.php @@ -0,0 +1,108 @@ +wrap($segments[0]).' AS '.$this->wrap($segments[2]); + } + + // Expressions should be injected into the query as raw strings, + // so we do not want to wrap them in any way. We'll just return + // the string value from the expression to be included. + if ($value instanceof Expression) return $value->get(); + + // Since columns may be prefixed with their corresponding table + // name so as to not make them ambiguous, we will need to wrap + // the table and the column in keyword identifiers. + foreach (explode('.', $value) as $segment) + { + if ($segment == '*') + { + $wrapped[] = $segment; + } + else + { + $wrapped[] = sprintf($this->wrapper, $segment); + } + } + + return implode('.', $wrapped); + } + + /** + * Create query parameters from an array of values. + * + * + * Returns "?, ?, ?", which may be used as PDO place-holders + * $parameters = $grammar->parameterize(array(1, 2, 3)); + * + * // Returns "?, "Taylor"" since an expression is used + * $parameters = $grammar->parameterize(array(1, DB::raw('Taylor'))); + * + * + * @param array $values + * @return string + */ + final public function parameterize($values) + { + return implode(', ', array_map(array($this, 'parameter'), $values)); + } + + /** + * Get the appropriate query parameter string for a value. + * + * + * // Returns a "?" PDO place-holder + * $value = $grammar->parameter('Taylor Otwell'); + * + * // Returns "Taylor Otwell" as the raw value of the expression + * $value = $grammar->parameter(DB::raw('Taylor Otwell')); + * + * + * @param mixed $value + * @return string + */ + final public function parameter($value) + { + return ($value instanceof Expression) ? $value->get() : '?'; + } + + /** + * Create a comma-delimited list of wrapped column names. + * + * + * // Returns ""Taylor", "Otwell"" when the identifier is quotes + * $columns = $grammar->columnize(array('Taylor', 'Otwell')); + * + * + * @param array $columns + * @return string + */ + final public function columnize($columns) + { + return implode(', ', array_map(array($this, 'wrap'), $columns)); + } + +} \ No newline at end of file diff --git a/laravel/database/query.php b/laravel/database/query.php index 93364675..fabeb4dd 100644 --- a/laravel/database/query.php +++ b/laravel/database/query.php @@ -1,4 +1,10 @@ -from = $table; $this->grammar = $grammar; @@ -156,7 +169,7 @@ public function left_join($table, $column1, $operator, $column2) } /** - * Reset the where clause to its initial state. All bindings will be cleared. + * Reset the where clause to its initial state. * * @return void */ @@ -203,8 +216,16 @@ public function raw_or_where($where, $bindings = array()) * @param string $connector * @return Query */ - public function where($column, $operator, $value, $connector = 'AND') + public function where($column, $operator = null, $value = null, $connector = 'AND') { + // If a CLosure is passed into the method, it means a nested where + // clause is being initiated, so we will take a different course + // of action than when the statement is just a simple where. + if ($column instanceof Closure) + { + return $this->where_nested($column, $connector); + } + $type = 'where'; $this->wheres[] = compact('type', 'column', 'operator', 'value', 'connector'); @@ -222,7 +243,7 @@ public function where($column, $operator, $value, $connector = 'AND') * @param mixed $value * @return Query */ - public function or_where($column, $operator, $value) + public function or_where($column, $operator = null, $value = null) { return $this->where($column, $operator, $value, 'OR'); } @@ -347,10 +368,35 @@ public function or_where_not_null($column) } /** - * Add dynamic where conditions to the query. + * Add a nested where condition to the query. * - * Dynamic queries are caught by the __call magic method and are parsed here. - * They provide a convenient, expressive API for building simple conditions. + * @param Closure $callback + * @param string $connector + * @return Query + */ + protected function where_nested($callback, $connector) + { + $type = 'where_nested'; + + // To handle a nested where statement, we will actually instantiate a + // new Query instance and run the callback over that instance, which + // will allow the developer to have a fresh query to work with. + $query = new Query($this->connection, $this->grammar, $this->from); + + // Once the callback has been run on the query, we will store the + // nested query instance on the where clause array so that it's + // passed to the query grammar. + call_user_func($callback, $query); + + $this->wheres[] = compact('type', 'query', 'connector'); + + $this->bindings = array_merge($this->bindings, $query->bindings); + + return $this; + } + + /** + * Add dynamic where conditions to the query. * * @param string $method * @param array $parameters @@ -358,11 +404,11 @@ public function or_where_not_null($column) */ private function dynamic_where($method, $parameters) { - // Strip the "where_" off of the method. $finder = substr($method, 6); - // Split the column names from the connectors. - $segments = preg_split('/(_and_|_or_)/i', $finder, -1, PREG_SPLIT_DELIM_CAPTURE); + $flags = PREG_SPLIT_DELIM_CAPTURE; + + $segments = preg_split('/(_and_|_or_)/i', $finder, -1, $flags); // The connector variable will determine which connector will be // used for the condition. We'll change it as we come across new @@ -377,6 +423,13 @@ private function dynamic_where($method, $parameters) foreach ($segments as $segment) { + // If the segment is not a boolean connector, we can assume it + // it is a column name, and we'll add it to the query as a new + // where clause. + // + // Otherwise, we'll store the connector so that we know how to + // connection the next where clause we find to the query, as + // all connectors should precede a new where clause. if ($segment != '_and_' and $segment != '_or_') { $this->where($segment, '=', $parameters[$index], $connector); @@ -392,6 +445,18 @@ private function dynamic_where($method, $parameters) return $this; } + /** + * Add a grouping to the query. + * + * @param string $column + * @return Query + */ + public function group_by($column) + { + $this->groupings[] = $column; + return $this; + } + /** * Add an ordering to the query. * @@ -430,7 +495,7 @@ public function take($value) } /** - * Set the query limit and offset for a given page and item per page count. + * Set the query limit and offset for a given page. * * @param int $page * @param int $per_page @@ -461,16 +526,14 @@ public function find($id, $columns = array('*')) */ public function only($column) { - $this->select(array($column)); + $sql = $this->grammar->select($this->select(array($column))); - return $this->connection->only($this->grammar->select($this), $this->bindings); + return $this->connection->only($sql, $this->bindings); } /** * Execute the query as a SELECT statement and return the first result. * - * If a single column is selected from the database, only the value of that column will be returned. - * * @param array $columns * @return mixed */ @@ -478,7 +541,51 @@ public function first($columns = array('*')) { $columns = (array) $columns; - return (count($results = $this->take(1)->get($columns)) > 0) ? $results[0] : null; + // Since we only need the first result, we'll go ahead and set the + // limit clause to 1, since this will be much faster than getting + // all of the rows and then only returning the first. + $results = $this->take(1)->get($columns); + + return (count($results) > 0) ? $results[0] : null; + } + + /** + * Get an array with the values of a given column. + * + * @param string $column + * @param string $key + * @return array + */ + public function lists($column, $key = null) + { + $columns = (is_null($key)) ? array($column) : array($column, $key); + + $results = $this->get($columns); + + // First we will get the array of values for the requested column. + // Of course, this array will simply have numeric keys. After we + // have this array we will determine if we need to key the array + // by another column from the result set. + $values = array_map(function($row) use ($column) + { + return $row->$column; + + }, $results); + + // If a key was provided, we will extract an array of keys and + // set the keys on the array of values using the array_combine + // function provided by PHP, which should give us the proper + // array form to return from the method. + if ( ! is_null($key)) + { + return array_combine(array_map(function($row) use ($key) + { + return $row->$key; + + }, $results), $values); + } + + return $values; } /** @@ -491,11 +598,25 @@ public function get($columns = array('*')) { if (is_null($this->selects)) $this->select($columns); - $results = $this->connection->query($this->grammar->select($this), $this->bindings); + $sql = $this->grammar->select($this); + + $results = $this->connection->query($sql, $this->bindings); + + // If the query has an offset and we are using the SQL Server grammar, + // we need to spin through the results and remove the "rownum" from + // each of the objects. Unfortunately SQL Server does not have an + // offset keyword, so we have to use row numbers in the query. + if ($this->offset > 0 and $this->grammar instanceof SQLServer) + { + array_walk($results, function($result) + { + unset($result->rownum); + }); + } // Reset the SELECT clause so more queries can be performed using // the same instance. This is helpful for getting aggregates and - // then getting actual results. + // then getting actual results from the query. $this->selects = null; return $results; @@ -512,11 +633,13 @@ private function aggregate($aggregator, $column) { $this->aggregate = compact('aggregator', 'column'); - $result = $this->connection->only($this->grammar->select($this), $this->bindings); + $sql = $this->grammar->select($this); + + $result = $this->connection->only($sql, $this->bindings); // Reset the aggregate so more queries can be performed using // the same instance. This is helpful for getting aggregates - // and then getting actual results. + // and then getting actual results from the query. $this->aggregate = null; return $result; @@ -531,17 +654,23 @@ private function aggregate($aggregator, $column) */ public function paginate($per_page = 20, $columns = array('*')) { - // Because some database engines may throw errors if we leave - // orderings on the query when retrieving the total number - // of records, we will remove all of the ordreings and put - // them back on the query after we have the count. + // Because some database engines may throw errors if we leave orderings + // on the query when retrieving the total number of records, we will + // remove all of the ordreings and put them back on the query after + // we have the count. list($orderings, $this->orderings) = array($this->orderings, null); $page = Paginator::page($total = $this->count(), $per_page); $this->orderings = $orderings; - return Paginator::make($this->for_page($page, $per_page)->get($columns), $total, $per_page); + // Now we're ready to get the actual pagination results from the + // database table. The "for_page" method provides a convenient + // way to set the limit and offset so we get the correct span + // of results from the table. + $results = $this->for_page($page, $per_page)->get($columns); + + return Paginator::make($results, $total, $per_page); } /** @@ -559,17 +688,21 @@ public function insert($values) $bindings = array(); + // We need to merge the the insert values into the array of the query + // bindings so that they will be bound to the PDO statement when it + // is executed by the database connection. foreach ($values as $value) { $bindings = array_merge($bindings, array_values($value)); } - return $this->connection->query($this->grammar->insert($this, $values), $bindings); + $sql = $this->grammar->insert($this, $values); + + return $this->connection->statement($sql, $bindings); } /** - * Insert an array of values into the database table and - * return the value of the ID column. + * Insert an array of values into the database table and return the ID. * * @param array $values * @param string $sequence @@ -577,8 +710,13 @@ public function insert($values) */ public function insert_get_id($values, $sequence = null) { - $this->connection->query($this->grammar->insert($this, $values), array_values($values)); + $sql = $this->grammar->insert($this, $values); + $this->connection->statement($sql, array_values($values)); + + // Some database systems (Postgres) require a sequence name to be + // given when retrieving the auto-incrementing ID, so we'll pass + // the given sequence into the method just in case. return (int) $this->connection->pdo->lastInsertId($sequence); } @@ -616,7 +754,10 @@ public function decrement($column, $amount = 1) */ protected function adjust($column, $amount, $operator) { - $value = Manager::raw($this->grammar->wrap($column).$operator.$amount); + // To make the adjustment to the column, we'll wrap the expression + // in an Expression instance, which forces the adjustment to be + // injected into the query as a string instead of bound. + $value = Database::raw($this->grammar->wrap($column).$operator.$amount); return $this->update(array($column => $value)); } @@ -629,9 +770,15 @@ protected function adjust($column, $amount, $operator) */ public function update($values) { + // For update statements, we need to merge the bindings such that + // the update values occur before the where bindings in the array + // since the set statements will precede any of the where clauses + // in the SQL syntax that is generated. $bindings = array_merge(array_values($values), $this->bindings); - return $this->connection->query($this->grammar->update($this, $values), $bindings); + $sql = $this->grammar->update($this, $values); + + return $this->connection->update($sql, $bindings); } /** @@ -644,16 +791,23 @@ public function update($values) */ public function delete($id = null) { - if ( ! is_null($id)) $this->where('id', '=', $id); + // If an ID is given to the method, we'll set the where clause + // to match on the value of the ID. This allows the developer + // to quickly delete a row by its primary key value. + if ( ! is_null($id)) + { + $this->where('id', '=', $id); + } - return $this->connection->query($this->grammar->delete($this), $this->bindings); + $sql = $this->grammar->delete($this); + + return $this->connection->delete($sql, $this->bindings); } /** * Magic Method for handling dynamic functions. * - * This method handles all calls to aggregate functions as well - * as the construction of dynamic where clauses. + * This method handles calls to aggregates as well as dynamic where clauses. */ public function __call($method, $parameters) { @@ -662,7 +816,7 @@ public function __call($method, $parameters) return $this->dynamic_where($method, $parameters, $this); } - if (in_array($method, array('abs', 'count', 'min', 'max', 'avg', 'sum'))) + if (in_array($method, array('count', 'min', 'max', 'avg', 'sum'))) { if ($method == 'count') { @@ -674,7 +828,7 @@ public function __call($method, $parameters) } } - throw new \BadMethodCallException("Method [$method] is not defined on the Query class."); + throw new \Exception("Method [$method] is not defined on the Query class."); } } diff --git a/laravel/database/grammars/grammar.php b/laravel/database/query/grammars/grammar.php similarity index 52% rename from laravel/database/grammars/grammar.php rename to laravel/database/query/grammars/grammar.php index 54b50616..3f01fbf4 100644 --- a/laravel/database/grammars/grammar.php +++ b/laravel/database/query/grammars/grammar.php @@ -1,61 +1,70 @@ -concatenate($this->components($query)); + } + /** + * Generate the SQL for every component of the query. + * + * @param Query $query + * @return array + */ + final protected function components($query) + { + // Each portion of the statement is compiled by a function corresponding + // to an item in the components array. This lets us to keep the creation + // of the query very granular, and allows for the flexible customization + // of the query building process by each database system's grammar. + // + // Note that each component also corresponds to a public property on the + // query instance, allowing us to pass the appropriate data into each of + // the compiler functions. foreach ($this->components as $component) { if ( ! is_null($query->$component)) { - $sql[] = call_user_func(array($this, $component), $query); + $sql[$component] = call_user_func(array($this, $component), $query); } } - return implode(' ', Arr::without($sql, array(null, ''))); + return (array) $sql; + } + + /** + * Concatenate an array of SQL segments, removing those that are empty. + * + * @param array $components + * @return string + */ + final protected function concatenate($components) + { + return implode(' ', array_filter($components, function($value) + { + return (string) $value !== ''; + })); } /** @@ -74,11 +83,6 @@ protected function selects(Query $query) /** * Compile an aggregating SELECT clause for a query. * - * If an aggregate function is called on the query instance, no select - * columns will be set, so it is safe to assume that the "selects" - * compiler function will not be called. We can simply build the - * aggregating select clause within this function. - * * @param Query $query * @return string */ @@ -92,9 +96,6 @@ protected function aggregate(Query $query) /** * Compile the FROM clause for a query. * - * This method should not handle the construction of "join" clauses. - * The join clauses will be constructured by their own compiler. - * * @param Query $query * @return string */ @@ -111,8 +112,13 @@ protected function from(Query $query) */ protected function joins(Query $query) { - $format = '%s JOIN %s ON %s %s %s'; - + // We need to iterate through each JOIN clause that is attached to the + // query an translate it into SQL. The table and the columns will be + // wrapped in identifiers to avoid naming collisions. + // + // Once all of the JOINs have been compiled, we can concatenate them + // together using a single space, which should give us the complete + // set of joins in valid SQL that can appended to the query. foreach ($query->joins as $join) { $table = $this->wrap($join['table']); @@ -121,7 +127,7 @@ protected function joins(Query $query) $column2 = $this->wrap($join['column2']); - $sql[] = sprintf($format, $join['type'], $table, $column1, $join['operator'], $column2); + $sql[] = "{$join['type']} JOIN {$table} ON {$column1} {$join['operator']} {$column2}"; } return implode(' ', $sql); @@ -136,26 +142,44 @@ protected function joins(Query $query) final protected function wheres(Query $query) { // Each WHERE clause array has a "type" that is assigned by the query - // builder, and each type has its own compiler function. We will simply - // iterate through the where clauses and call the appropriate compiler - // for each clause. + // builder, and each type has its own compiler function. We will call + // the appropriate compiler for each where clause in the query. + // + // Keeping each particular where clause in its own "compiler" allows + // us to keep the query generation process very granular, making it + // easier to customize derived grammars for other databases. foreach ($query->wheres as $where) { $sql[] = $where['connector'].' '.$this->{$where['type']}($where); } - if (isset($sql)) return implode(' ', array_merge(array('WHERE 1 = 1'), $sql)); + if (isset($sql)) + { + // We attach the boolean connector to every where segment just + // for convenience. Once we have built the entire clause we'll + // remove the first instance of a connector from the clause. + return 'WHERE '.preg_replace('/AND |OR /', '', implode(' ', $sql), 1); + } + } + + /** + * Compile a nested WHERE clause. + * + * @param array $where + * @return string + */ + protected function where_nested($where) + { + // To generate a nested WHERE clause, we'll just feed the query + // back into the "wheres" method. Once we have the clause, we + // will strip off the first six characters to get rid of the + // leading WHERE keyword. + return '('.substr($this->wheres($where['query']), 6).')'; } /** * Compile a simple WHERE clause. * - * This method handles the compilation of the structures created by the - * "where" and "or_where" methods on the query builder. - * - * This method also handles database expressions, so care must be taken - * to implement this functionality in any derived database grammars. - * * @param array $where * @return string */ @@ -220,11 +244,22 @@ protected function where_not_null($where) * @param array $where * @return string */ - protected function where_raw($where) + final protected function where_raw($where) { return $where['sql']; } + /** + * Compile the GROUP BY clause for a query. + * + * @param Query $query + * @return string + */ + protected function groupings(Query $query) + { + return 'GROUP BY '.$this->columnize($query->groupings); + } + /** * Compile the ORDER BY clause for a query. * @@ -235,7 +270,9 @@ protected function orderings(Query $query) { foreach ($query->orderings as $ordering) { - $sql[] = $this->wrap($ordering['column']).' '.strtoupper($ordering['direction']); + $direction = strtoupper($ordering['direction']); + + $sql[] = $this->wrap($ordering['column']).' '.$direction; } return 'ORDER BY '.implode(', ', $sql); @@ -266,7 +303,7 @@ protected function offset(Query $query) /** * Compile a SQL INSERT statment from a Query instance. * - * Note: This method handles the compilation of single row inserts and batch inserts. + * This method handles the compilation of single row inserts and batch inserts. * * @param Query $query * @param array $values @@ -274,14 +311,16 @@ protected function offset(Query $query) */ public function insert(Query $query, $values) { - // Force every insert to be treated like a batch insert. This simple makes + $table = $this->wrap($query->from); + + // Force every insert to be treated like a batch insert. This simply makes // creating the SQL syntax a little easier on us since we can always treat // the values as if it is an array containing multiple inserts. if ( ! is_array(reset($values))) $values = array($values); // Since we only care about the column names, we can pass any of the insert - // arrays into the "columnize" method. The names should be the same for - // every insert to the table. + // arrays into the "columnize" method. The columns should be the same for + // every insert to the table so we can just use the first record. $columns = $this->columnize(array_keys(reset($values))); // Build the list of parameter place-holders of values bound to the query. @@ -289,24 +328,26 @@ public function insert(Query $query, $values) // just use the first array of values. $parameters = $this->parameterize(reset($values)); - $parameters = implode(', ', array_fill(0, count($values), '('.$parameters.')')); + $parameters = implode(', ', array_fill(0, count($values), "($parameters)")); - return 'INSERT INTO '.$this->wrap($query->from).' ('.$columns.') VALUES '.$parameters; + return "INSERT INTO {$table} ({$columns}) VALUES {$parameters}"; } /** * Compile a SQL UPDATE statment from a Query instance. * - * Note: Since UPDATE statements can be limited by a WHERE clause, - * this method will use the same WHERE clause compilation - * functions as the "select" method. - * * @param Query $query * @param array $values * @return string */ public function update(Query $query, $values) { + $table = $this->wrap($query->from); + + // Each column in the UPDATE statement needs to be wrapped in keyword + // identifiers, and a place-holder needs to be created for each value + // in the array of bindings. Of course, if the value of the binding + // is an expression, the expression string will be injected. foreach ($values as $column => $value) { $columns[] = $this->wrap($column).' = '.$this->parameter($value); @@ -314,7 +355,11 @@ public function update(Query $query, $values) $columns = implode(', ', $columns); - return trim('UPDATE '.$this->wrap($query->from).' SET '.$columns.' '.$this->wheres($query)); + // UPDATE statements may be constrained by a WHERE clause, so we'll + // run the entire where compilation process for those contraints. + // This is easily achieved by passing the query to the "wheres" + // method which will call all of the where compilers. + return trim("UPDATE {$table} SET {$columns} ".$this->wheres($query)); } /** @@ -325,103 +370,13 @@ public function update(Query $query, $values) */ public function delete(Query $query) { - return trim('DELETE FROM '.$this->wrap($query->from).' '.$this->wheres($query)); - } + $table = $this->wrap($query->from); - /** - * The following functions primarily serve as utility functions for - * the grammar. They perform tasks such as wrapping values in keyword - * identifiers or creating variable lists of bindings. - */ - - /** - * Create a comma-delimited list of wrapped column names. - * - * @param array $columns - * @return string - */ - final public function columnize($columns) - { - return implode(', ', array_map(array($this, 'wrap'), $columns)); - } - - /** - * Wrap a value in keyword identifiers. - * - * They keyword identifier used by the method is specified as - * a property on the grammar class so it can be conveniently - * overriden without changing the wrapping logic itself. - * - * @param string $value - * @return string - */ - public function wrap($value) - { - // If the value being wrapped contains a column alias, we need to wrap - // it a little differently since each segment must be wrapped and not - // the entire string. - if (strpos(strtolower($value), ' as ') !== false) - { - return $this->alias($value); - } - - // Expressions should be injected into the query as raw strings, so we - // do not want to wrap them in any way. We will just return the string - // value from the expression to be included in the query. - if ($value instanceof Expression) return $value->get(); - - foreach (explode('.', $value) as $segment) - { - if ($segment === '*') - { - $wrapped[] = $segment; - } - else - { - $wrapped[] = $this->wrapper.$segment.$this->wrapper; - } - } - - return implode('.', $wrapped); - } - - /** - * Wrap an alias in keyword identifiers. - * - * @param string $value - * @return string - */ - protected function alias($value) - { - $segments = explode(' ', $value); - - return $this->wrap($segments[0]).' AS '.$this->wrap($segments[2]); - } - - /** - * Create query parameters from an array of values. - * - * @param array $values - * @return string - */ - public function parameterize($values) - { - return implode(', ', array_map(array($this, 'parameter'), $values)); - } - - /** - * Get the appropriate query parameter string for a value. - * - * If the value is an expression, the raw expression string should - * be returned, otherwise, the parameter place-holder will be - * returned by the method. - * - * @param mixed $value - * @return string - */ - public function parameter($value) - { - return ($value instanceof Expression) ? $value->get() : '?'; + // Like the UPDATE statement, the DELETE statement is constrained + // by WHERE clauses, so we'll need to run the "wheres" method to + // make the WHERE clauses for the query. The "wheres" method + // encapsulates the logic to create the full WHERE clause. + return trim("DELETE FROM {$table} ".$this->wheres($query)); } } \ No newline at end of file diff --git a/laravel/database/grammars/mysql.php b/laravel/database/query/grammars/mysql.php similarity index 59% rename from laravel/database/grammars/mysql.php rename to laravel/database/query/grammars/mysql.php index 8c45550f..37692e0a 100644 --- a/laravel/database/grammars/mysql.php +++ b/laravel/database/query/grammars/mysql.php @@ -1,4 +1,4 @@ -offset > 0) + { + return $this->ansi_offset($query, $sql); + } + + // Once all of the clauses have been compiled, we can join them all as + // one statement. Any segments that are null or an empty string will + // be removed from the array of clauses before they are imploded. + return $this->concatenate($sql); + } + + /** + * Compile the SELECT clause for a query. + * + * @param Query $query + * @return string + */ + protected function selects(Query $query) + { + $select = ($query->distinct) ? 'SELECT DISTINCT ' : 'SELECT '; + + // Instead of using a "LIMIT" keyword, SQL Server uses the "TOP" + // keyword within the SELECT statement. So, if we have a limit, + // we will add it here. + // + // We will not add the TOP clause if there is an offset however, + // since we will have to handle offsets using the ANSI syntax + // and will need to remove the TOP clause in that situation. + if ($query->limit > 0 and $query->offset <= 0) + { + $select .= 'TOP '.$query->limit.' '; + } + + return $select.$this->columnize($query->selects); + } + + /** + * Generate the ANSI standard SQL for an offset clause. + * + * @param Query $query + * @param array $components + * @return array + */ + protected function ansi_offset(Query $query, $components) + { + // An ORDER BY clause is required to make this offset query + // work, so if one doesn't exist, we'll just create a dummy + // clause to satisfy the database. + if ( ! isset($components['orderings'])) + { + $components['orderings'] = 'ORDER BY (SELECT 0)'; + } + + // We need to add the row number to the query results so we + // can compare it against the offset and limit values given + // for the statement. To do that we'll add an expression to + // the select statement for the row number. + $orderings = $components['orderings']; + + $components['selects'] .= ", ROW_NUMBER() OVER ({$orderings}) AS RowNum"; + + unset($components['orderings']); + + $start = $query->offset + 1; + + // Next we need to calculate the constraint that should be + // placed on the row number to get the correct offset and + // limit on the query. If a limit has not been set, we'll + // only add a constraint to handle offset. + if ($query->limit > 0) + { + $finish = $query->offset + $query->limit; + + $constraint = "BETWEEN {$start} AND {$finish}"; + } + else + { + $constraint = ">= {$start}"; + } + + // Now, we're finally ready to build the final SQL query. + // We'll create a common table expression with the query + // and then select all of the results from it where the + // row number is between oru given limit and offset. + $sql = $this->concatenate($components); + + return "SELECT * FROM ($sql) AS TempTable WHERE RowNum {$constraint}"; + } + + /** + * Compile the LIMIT clause for a query. + * + * @param Query $query + * @return string + */ + protected function limit(Query $query) + { + return; + } + + /** + * Compile the OFFSET clause for a query. + * + * @param Query $query + * @return string + */ + protected function offset(Query $query) + { + return; + } + +} \ No newline at end of file diff --git a/laravel/database/schema.php b/laravel/database/schema.php new file mode 100644 index 00000000..84de3eb0 --- /dev/null +++ b/laravel/database/schema.php @@ -0,0 +1,119 @@ +commands as $command) + { + $connection = DB::connection($table->connection); + + $grammar = static::grammar($connection->driver()); + + // Each grammar has a function that corresponds to the command type + // and is responsible for building that's commands SQL. This lets + // the SQL generation stay very granular and makes it simply to + // add new database systems to the schema system. + if (method_exists($grammar, $method = $command->type)) + { + $statements = $grammar->$method($table, $command); + + // Once we have the statements, we will cast them to an array even + // though not all of the commands return an array. This is just in + // case the command needs to run more than one query to do what + // it needs to do what is requested by the developer. + foreach ((array) $statements as $statement) + { + $connection->statement($statement); + } + } + } + } + + /** + * Add any implicit commands to the schema table operation. + * + * @param Schema\Table $table + * @return void + */ + protected static function implications($table) + { + // If the developer has specified columns for the table and the + // table is not being created, we will assume they simply want + // to add the columns to the table, and will generate an add + // command for them, adding the columns to the command. + if (count($table->columns) > 0 and ! $table->creating()) + { + $command = new Fluent(array('type' => 'add')); + + array_unshift($table->commands, $command); + } + + // For some extra syntax sugar, we'll check for any implicit + // indexes on the table. The developer may specify the index + // type on the fluent column declaration. Here we'll find + // any such implicit index and add the actual command. + foreach ($table->columns as $column) + { + foreach (array('primary', 'unique', 'fulltext', 'index') as $key) + { + if ($column->$key === true) + { + $table->$key($column->name); + } + } + } + } + + /** + * Create the appropriate schema grammar for the driver. + * + * @param string $driver + * @return Grammar + */ + public static function grammar($driver) + { + switch ($driver) + { + case 'mysql': + return new Schema\Grammars\MySQL; + + case 'pgsql': + return new Schema\Grammars\Postgres; + + case 'sqlsrv': + return new Schema\Grammars\SQLServer; + + case 'sqlite': + return new Schema\Grammars\SQLite; + } + + throw new \Exception("Schema operations not supported for [$driver]."); + } + +} \ No newline at end of file diff --git a/laravel/database/schema/grammars/grammar.php b/laravel/database/schema/grammars/grammar.php new file mode 100644 index 00000000..e09d2986 --- /dev/null +++ b/laravel/database/schema/grammars/grammar.php @@ -0,0 +1,38 @@ +{'type_'.$column->type}($column); + } + + /** + * Wrap a value in keyword identifiers. + * + * @param Table|string $value + * @return string + */ + public function wrap($value) + { + // This method is primarily for convenience so we can just pass a + // column or table instance into the wrap method without sending + // in the name each time we need to wrap one of these objects. + if ($value instanceof Table or $value instanceof Fluent) + { + $value = $value->name; + } + + return parent::wrap($value); + } + +} \ No newline at end of file diff --git a/laravel/database/schema/grammars/mysql.php b/laravel/database/schema/grammars/mysql.php new file mode 100644 index 00000000..89bd1b95 --- /dev/null +++ b/laravel/database/schema/grammars/mysql.php @@ -0,0 +1,386 @@ +columns($table)); + + // First we will generate the base table creation statement. Other than + // auto-incrementing keys, no indexes will be created during the first + // creation of the table. They will be added in separate commands. + $sql = 'CREATE TABLE '.$this->wrap($table).' ('.$columns.')'; + + // MySQL supports various "engines" for database tables. If an engine + // was specified by the developer, we will set it after adding the + // columns the table creation statement. + if ( ! is_null($table->engine)) + { + $sql .= ' ENGINE = '.$table->engine; + } + + return $sql; + } + + /** + * Geenrate the SQL statements for a table modification command. + * + * @param Table $table + * @param Fluent $command + * @return array + */ + public function add(Table $table, Fluent $command) + { + $columns = $this->columns($table); + + // Once we the array of column definitions, we need to add "add" + // to the front of each definition, then we'll concatenate the + // definitions using commas like normal and generate the SQL. + $columns = implode(', ', array_map(function($column) + { + return 'ADD '.$column; + + }, $columns)); + + return 'ALTER TABLE '.$this->wrap($table).' '.$columns; + } + + /** + * Create the individual column definitions for the table. + * + * @param Table $table + * @return array + */ + protected function columns(Table $table) + { + $columns = array(); + + foreach ($table->columns as $column) + { + // Each of the data type's have their own definition creation method, + // which is responsible for creating the SQL for the type. This lets + // us to keep the syntax easy and fluent, while translating the + // types to the types used by the database. + $sql = $this->wrap($column).' '.$this->type($column); + + $elements = array('nullable', 'defaults', 'incrementer'); + + foreach ($elements as $element) + { + $sql .= $this->$element($table, $column); + } + + $columns[] = $sql; + } + + return $columns; + } + + /** + * Get the SQL syntax for indicating if a column is nullable. + * + * @param Table $table + * @param Fluent $column + * @return string + */ + protected function nullable(Table $table, Fluent $column) + { + return ($column->nullable) ? ' NULL' : ' NOT NULL'; + } + + /** + * Get the SQL syntax for specifying a default value on a column. + * + * @param Table $table + * @param Fluent $column + * @return string + */ + protected function defaults(Table $table, Fluent $column) + { + if ( ! is_null($column->default)) + { + return " DEFAULT '".$column->default."'"; + } + } + + /** + * Get the SQL syntax for defining an auto-incrementing column. + * + * @param Table $table + * @param Fluent $column + * @return string + */ + protected function incrementer(Table $table, Fluent $column) + { + if ($column->type == 'integer' and $column->increment) + { + return ' AUTO_INCREMENT PRIMARY KEY'; + } + } + + /** + * Generate the SQL statement for creating a primary key. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function primary(Table $table, Fluent $command) + { + return $this->key($table, $command, 'PRIMARY KEY'); + } + + /** + * Generate the SQL statement for creating a unique index. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function unique(Table $table, Fluent $command) + { + return $this->key($table, $command, 'UNIQUE'); + } + + /** + * Generate the SQL statement for creating a full-text index. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function fulltext(Table $table, Fluent $command) + { + return $this->key($table, $command, 'FULLTEXT'); + } + + /** + * Generate the SQL statement for creating a regular index. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function index(Table $table, Fluent $command) + { + return $this->key($table, $command, 'INDEX'); + } + + /** + * Generate the SQL statement for creating a new index. + * + * @param Table $table + * @param Fluent $command + * @param string $type + * @return string + */ + protected function key(Table $table, Fluent $command, $type) + { + $keys = $this->columnize($command->columns); + + $name = $command->name; + + return 'ALTER TABLE '.$this->wrap($table)." ADD {$type} {$name}({$keys})"; + } + + /** + * Generate the SQL statement for a drop table command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function drop(Table $table, Fluent $command) + { + return 'DROP TABLE '.$this->wrap($table); + } + + /** + * Generate the SQL statement for a drop column command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function drop_column(Table $table, Fluent $command) + { + $columns = array_map(array($this, 'wrap'), $command->columns); + + // Once we the array of column names, we need to add "drop" to the + // front of each column, then we'll concatenate the columns using + // commas and generate the alter statement SQL. + $columns = implode(', ', array_map(function($column) + { + return 'DROP '.$column; + + }, $columns)); + + return 'ALTER TABLE '.$this->wrap($table).' '.$columns; + } + + /** + * Generate the SQL statement for a drop primary key command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function drop_primary(Table $table, Fluent $command) + { + return 'ALTER TABLE '.$this->wrap($table).' DROP PRIMARY KEY'; + } + + /** + * Generate the SQL statement for a drop unqique key command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function drop_unique(Table $table, Fluent $command) + { + return $this->drop_key($table, $command); + } + + /** + * Generate the SQL statement for a drop full-text key command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function drop_fulltext(Table $table, Fluent $command) + { + return $this->drop_key($table, $command); + } + + /** + * Generate the SQL statement for a drop unqique key command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function drop_index(Table $table, Fluent $command) + { + return $this->drop_key($table, $command); + } + + /** + * Generate the SQL statement for a drop key command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + protected function drop_key(Table $table, Fluent $command) + { + return 'ALTER TABLE '.$this->wrap($table)." DROP INDEX {$command->name}"; + } + + /** + * Generate the data-type definition for a string. + * + * @param Fluent $column + * @return string + */ + protected function type_string(Fluent $column) + { + return 'VARCHAR('.$column->length.')'; + } + + /** + * Generate the data-type definition for an integer. + * + * @param Fluent $column + * @return string + */ + protected function type_integer(Fluent $column) + { + return 'INT'; + } + + /** + * Generate the data-type definition for an integer. + * + * @param Fluent $column + * @return string + */ + protected function type_float(Fluent $column) + { + return 'FLOAT'; + } + + /** + * Generate the data-type definition for a boolean. + * + * @param Fluent $column + * @return string + */ + protected function type_boolean(Fluent $column) + { + return 'TINYINT'; + } + + /** + * Generate the data-type definition for a date. + * + * @param Fluent $column + * @return string + */ + protected function type_date(Fluent $column) + { + return 'DATETIME'; + } + + /** + * Generate the data-type definition for a timestamp. + * + * @param Fluent $column + * @return string + */ + protected function type_timestamp(Fluent $column) + { + return 'TIMESTAMP'; + } + + /** + * Generate the data-type definition for a text column. + * + * @param Fluent $column + * @return string + */ + protected function type_text(Fluent $column) + { + return 'TEXT'; + } + + /** + * Generate the data-type definition for a blob. + * + * @param Fluent $column + * @return string + */ + protected function type_blob(Fluent $column) + { + return 'BLOB'; + } + +} \ No newline at end of file diff --git a/laravel/database/schema/grammars/postgres.php b/laravel/database/schema/grammars/postgres.php new file mode 100644 index 00000000..1fb6d21a --- /dev/null +++ b/laravel/database/schema/grammars/postgres.php @@ -0,0 +1,381 @@ +columns($table)); + + // First we will generate the base table creation statement. Other than + // auto-incrementing keys, no indexes will be created during the first + // creation of the table. They will be added in separate commands. + $sql = 'CREATE TABLE '.$this->wrap($table).' ('.$columns.')'; + + return $sql; + } + + /** + * Geenrate the SQL statements for a table modification command. + * + * @param Table $table + * @param Fluent $command + * @return array + */ + public function add(Table $table, Fluent $command) + { + $columns = $this->columns($table); + + // Once we the array of column definitions, we'll add "add column" + // to the front of each definition, then we'll concatenate the + // definitions using commas like normal and generate the SQL. + $columns = implode(', ', array_map(function($column) + { + return 'ADD COLUMN '.$column; + + }, $columns)); + + return 'ALTER TABLE '.$this->wrap($table).' '.$columns; + } + + /** + * Create the individual column definitions for the table. + * + * @param Table $table + * @return array + */ + protected function columns(Table $table) + { + $columns = array(); + + foreach ($table->columns as $column) + { + // Each of the data type's have their own definition creation method, + // which is responsible for creating the SQL for the type. This lets + // us to keep the syntax easy and fluent, while translating the + // types to the types used by the database. + $sql = $this->wrap($column).' '.$this->type($column); + + $elements = array('incrementer', 'nullable', 'defaults'); + + foreach ($elements as $element) + { + $sql .= $this->$element($table, $column); + } + + $columns[] = $sql; + } + + return $columns; + } + + /** + * Get the SQL syntax for indicating if a column is nullable. + * + * @param Table $table + * @param Fluent $column + * @return string + */ + protected function nullable(Table $table, Fluent $column) + { + return ($column->nullable) ? ' NULL' : ' NOT NULL'; + } + + /** + * Get the SQL syntax for specifying a default value on a column. + * + * @param Table $table + * @param Fluent $column + * @return string + */ + protected function defaults(Table $table, Fluent $column) + { + if ( ! is_null($column->default)) + { + return " DEFAULT '".$column->default."'"; + } + } + + /** + * Get the SQL syntax for defining an auto-incrementing column. + * + * @param Table $table + * @param Fluent $column + * @return string + */ + protected function incrementer(Table $table, Fluent $column) + { + // We don't actually need to specify an "auto_increment" keyword since + // we handle the auto-increment definition in the type definition for + // integers by changing the type to "serial", which is a convenient + // notational short-cut provided by Postgres. + if ($column->type == 'integer' and $column->increment) + { + return ' PRIMARY KEY'; + } + } + + /** + * Generate the SQL statement for creating a primary key. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function primary(Table $table, Fluent $command) + { + $columns = $this->columnize($command->columns); + + return 'ALTER TABLE '.$this->wrap($table)." ADD PRIMARY KEY ({$columns})"; + } + + /** + * Generate the SQL statement for creating a unique index. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function unique(Table $table, Fluent $command) + { + return $this->key($table, $command, true); + } + + /** + * Generate the SQL statement for creating a full-text index. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function fulltext(Table $table, Fluent $command) + { + $name = $command->name; + + $columns = $this->columnize($command->columns); + + return "CREATE INDEX {$name} ON ".$this->wrap($table)." USING gin({$columns})"; + } + + /** + * Generate the SQL statement for creating a regular index. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function index(Table $table, Fluent $command) + { + return $this->key($table, $command); + } + + /** + * Generate the SQL statement for creating a new index. + * + * @param Table $table + * @param Fluent $command + * @param bool $unique + * @return string + */ + protected function key(Table $table, Fluent $command, $unique = false) + { + $columns = $this->columnize($command->columns); + + $create = ($unique) ? 'CREATE UNIQUE' : 'CREATE'; + + return $create." INDEX {$command->name} ON ".$this->wrap($table)." ({$columns})"; + } + + /** + * Generate the SQL statement for a drop table command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function drop(Table $table, Fluent $command) + { + return 'DROP TABLE '.$this->wrap($table); + } + + /** + * Generate the SQL statement for a drop column command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function drop_column(Table $table, Fluent $command) + { + $columns = array_map(array($this, 'wrap'), $command->columns); + + // Once we the array of column names, we need to add "drop" to the + // front of each column, then we'll concatenate the columns using + // commas and generate the alter statement SQL. + $columns = implode(', ', array_map(function($column) + { + return 'DROP COLUMN '.$column; + + }, $columns)); + + return 'ALTER TABLE '.$this->wrap($table).' '.$columns; + } + + /** + * Generate the SQL statement for a drop primary key command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function drop_primary(Table $table, Fluent $command) + { + return 'ALTER TABLE '.$this->wrap($table).' DROP CONSTRAINT '.$table->name.'_pkey'; + } + + /** + * Generate the SQL statement for a drop unqique key command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function drop_unique(Table $table, Fluent $command) + { + return $this->drop_key($table, $command); + } + + /** + * Generate the SQL statement for a drop full-text key command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function drop_fulltext(Table $table, Fluent $command) + { + return $this->drop_key($table, $command); + } + + /** + * Generate the SQL statement for a drop index command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function drop_index(Table $table, Fluent $command) + { + return $this->drop_key($table, $command); + } + + /** + * Generate the SQL statement for a drop key command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + protected function drop_key(Table $table, Fluent $command) + { + return 'DROP INDEX '.$command->name; + } + + /** + * Generate the data-type definition for a string. + * + * @param Fluent $column + * @return string + */ + protected function type_string(Fluent $column) + { + return 'VARCHAR('.$column->length.')'; + } + + /** + * Generate the data-type definition for an integer. + * + * @param Fluent $column + * @return string + */ + protected function type_integer(Fluent $column) + { + return ($column->increment) ? 'SERIAL' : 'INTEGER'; + } + + /** + * Generate the data-type definition for an integer. + * + * @param Fluent $column + * @return string + */ + protected function type_float(Fluent $column) + { + return 'REAL'; + } + + /** + * Generate the data-type definition for a boolean. + * + * @param Fluent $column + * @return string + */ + protected function type_boolean(Fluent $column) + { + return 'SMALLINT'; + } + + /** + * Generate the data-type definition for a date. + * + * @param Fluent $column + * @return string + */ + protected function type_date(Fluent $column) + { + return 'TIMESTAMP'; + } + + /** + * Generate the data-type definition for a timestamp. + * + * @param Fluent $column + * @return string + */ + protected function type_timestamp(Fluent $column) + { + return 'TIMESTAMP'; + } + + /** + * Generate the data-type definition for a text column. + * + * @param Fluent $column + * @return string + */ + protected function type_text(Fluent $column) + { + return 'TEXT'; + } + + /** + * Generate the data-type definition for a blob. + * + * @param Fluent $column + * @return string + */ + protected function type_blob(Fluent $column) + { + return 'BYTEA'; + } + +} \ No newline at end of file diff --git a/laravel/database/schema/grammars/sqlite.php b/laravel/database/schema/grammars/sqlite.php new file mode 100644 index 00000000..b86b9fb2 --- /dev/null +++ b/laravel/database/schema/grammars/sqlite.php @@ -0,0 +1,344 @@ +columns($table)); + + // First we will generate the base table creation statement. Other than + // auto-incrementing keys, no indexes will be created during the first + // creation of the table. They will be added in separate commands. + $sql = 'CREATE TABLE '.$this->wrap($table).' ('.$columns; + + // SQLite does not allow adding a primary key as a command apart from + // when the table is initially created, so we'll need to sniff out + // any primary keys here and add them to the table. + // + // Because of this, this class does not have the typical "primary" + // method as it would be pointless since the primary keys can't + // be set on anything but the table creation statement. + $primary = array_first($table->commands, function($key, $value) + { + return $value->type == 'primary'; + }); + + // If we found primary key in the array of commands, we'll create + // the SQL for the key addition and append it to the SQL table + // creation statement for the schema table. + if ( ! is_null($primary)) + { + $columns = $this->columnize($primary->columns); + + $sql .= ", PRIMARY KEY ({$columns})"; + } + + return $sql .= ')'; + } + + /** + * Geenrate the SQL statements for a table modification command. + * + * @param Table $table + * @param Fluent $command + * @return array + */ + public function add(Table $table, Fluent $command) + { + $columns = $this->columns($table); + + // Once we have an array of all of the column definitions, we need to + // spin through each one and prepend "ADD COLUMN" to each of them, + // which is the syntax used by SQLite when adding columns. + $columns = array_map(function($column) + { + return 'ADD COLUMN '.$column; + + }, $columns); + + // SQLite only allows one column to be added in an ALTER statement, + // so we will create an array of statements and return them all to + // the schema manager, which will execute each one. + foreach ($columns as $column) + { + $sql[] = 'ALTER TABLE '.$this->wrap($table).' '.$column; + } + + return (array) $sql; + } + + /** + * Create the individual column definitions for the table. + * + * @param Table $table + * @return array + */ + protected function columns(Table $table) + { + $columns = array(); + + foreach ($table->columns as $column) + { + // Each of the data type's have their own definition creation method + // which is responsible for creating the SQL for the type. This lets + // us to keep the syntax easy and fluent, while translating the + // types to the types used by the database. + $sql = $this->wrap($column).' '.$this->type($column); + + $elements = array('nullable', 'defaults', 'incrementer'); + + foreach ($elements as $element) + { + $sql .= $this->$element($table, $column); + } + + $columns[] = $sql; + } + + return $columns; + } + + /** + * Get the SQL syntax for indicating if a column is nullable. + * + * @param Table $table + * @param Fluent $column + * @return string + */ + protected function nullable(Table $table, Fluent $column) + { + return ($column->nullable) ? ' NULL' : ' NOT NULL'; + } + + /** + * Get the SQL syntax for specifying a default value on a column. + * + * @param Table $table + * @param Fluent $column + * @return string + */ + protected function defaults(Table $table, Fluent $column) + { + if ( ! is_null($column->default)) + { + return ' DEFAULT '.$this->wrap($column->default); + } + } + + /** + * Get the SQL syntax for defining an auto-incrementing column. + * + * @param Table $table + * @param Fluent $column + * @return string + */ + protected function incrementer(Table $table, Fluent $column) + { + if ($column->type == 'integer' and $column->increment) + { + return ' PRIMARY KEY AUTOINCREMENT'; + } + } + + /** + * Generate the SQL statement for creating a unique index. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function unique(Table $table, Fluent $command) + { + return $this->key($table, $command, true); + } + + /** + * Generate the SQL statement for creating a full-text index. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function fulltext(Table $table, Fluent $command) + { + $columns = $this->columnize($command->columns); + + return 'CREATE VIRTUAL TABLE '.$this->wrap($table)." USING fts4({$columns})"; + } + + /** + * Generate the SQL statement for creating a regular index. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function index(Table $table, Fluent $command) + { + return $this->key($table, $command); + } + + /** + * Generate the SQL statement for creating a new index. + * + * @param Table $table + * @param Fluent $command + * @param bool $unique + * @return string + */ + protected function key(Table $table, Fluent $command, $unique = false) + { + $columns = $this->columnize($command->columns); + + $create = ($unique) ? 'CREATE UNIQUE' : 'CREATE'; + + return $create." INDEX {$command->name} ON ".$this->wrap($table)." ({$columns})"; + } + + /** + * Generate the SQL statement for a drop table command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function drop(Table $table, Fluent $command) + { + return 'DROP TABLE '.$this->wrap($table); + } + + /** + * Generate the SQL statement for a drop unqique key command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function drop_unique(Table $table, Fluent $command) + { + return $this->drop_key($table, $command); + } + + /** + * Generate the SQL statement for a drop unqique key command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function drop_index(Table $table, Fluent $command) + { + return $this->drop_key($table, $command); + } + + /** + * Generate the SQL statement for a drop key command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + protected function drop_key(Table $table, Fluent $command) + { + return 'DROP INDEX '.$this->wrap($command->name); + } + + /** + * Generate the data-type definition for a string. + * + * @param Fluent $column + * @return string + */ + protected function type_string(Fluent $column) + { + return 'VARCHAR'; + } + + /** + * Generate the data-type definition for an integer. + * + * @param Fluent $column + * @return string + */ + protected function type_integer(Fluent $column) + { + return 'INTEGER'; + } + + /** + * Generate the data-type definition for an integer. + * + * @param Fluent $column + * @return string + */ + protected function type_float(Fluent $column) + { + return 'FLOAT'; + } + + /** + * Generate the data-type definition for a boolean. + * + * @param Fluent $column + * @return string + */ + protected function type_boolean(Fluent $column) + { + return 'INTEGER'; + } + + /** + * Generate the data-type definition for a date. + * + * @param Fluent $column + * @return string + */ + protected function type_date(Fluent $column) + { + return 'DATETIME'; + } + + /** + * Generate the data-type definition for a timestamp. + * + * @param Fluent $column + * @return string + */ + protected function type_timestamp(Fluent $column) + { + return 'DATETIME'; + } + + /** + * Generate the data-type definition for a text column. + * + * @param Fluent $column + * @return string + */ + protected function type_text(Fluent $column) + { + return 'TEXT'; + } + + /** + * Generate the data-type definition for a blob. + * + * @param Fluent $column + * @return string + */ + protected function type_blob(Fluent $column) + { + return 'BLOB'; + } + +} \ No newline at end of file diff --git a/laravel/database/schema/grammars/sqlserver.php b/laravel/database/schema/grammars/sqlserver.php new file mode 100644 index 00000000..b4b5342c --- /dev/null +++ b/laravel/database/schema/grammars/sqlserver.php @@ -0,0 +1,402 @@ +columns($table)); + + // First we will generate the base table creation statement. Other than + // auto-incrementing keys, no indexes will be created during the first + // creation of the table. They will be added in separate commands. + $sql = 'CREATE TABLE '.$this->wrap($table).' ('.$columns.')'; + + return $sql; + } + + /** + * Geenrate the SQL statements for a table modification command. + * + * @param Table $table + * @param Fluent $command + * @return array + */ + public function add(Table $table, Fluent $command) + { + $columns = $this->columns($table); + + // Once we the array of column definitions, we need to add "add" + // to the front of each definition, then we'll concatenate the + // definitions using commas like normal and generate the SQL. + $columns = implode(', ', array_map(function($column) + { + return 'ADD '.$column; + + }, $columns)); + + return 'ALTER TABLE '.$this->wrap($table).' '.$columns; + } + + /** + * Create the individual column definitions for the table. + * + * @param Table $table + * @return array + */ + protected function columns(Table $table) + { + $columns = array(); + + foreach ($table->columns as $column) + { + // Each of the data type's have their own definition creation method, + // which is responsible for creating the SQL for the type. This lets + // us to keep the syntax easy and fluent, while translating the + // types to the types used by the database. + $sql = $this->wrap($column).' '.$this->type($column); + + $elements = array('incrementer', 'nullable', 'defaults'); + + foreach ($elements as $element) + { + $sql .= $this->$element($table, $column); + } + + $columns[] = $sql; + } + + return $columns; + } + + /** + * Get the SQL syntax for indicating if a column is nullable. + * + * @param Table $table + * @param Fluent $column + * @return string + */ + protected function nullable(Table $table, Fluent $column) + { + return ($column->nullable) ? ' NULL' : ' NOT NULL'; + } + + /** + * Get the SQL syntax for specifying a default value on a column. + * + * @param Table $table + * @param Fluent $column + * @return string + */ + protected function defaults(Table $table, Fluent $column) + { + if ( ! is_null($column->default)) + { + return " DEFAULT '".$column->default."'"; + } + } + + /** + * Get the SQL syntax for defining an auto-incrementing column. + * + * @param Table $table + * @param Fluent $column + * @return string + */ + protected function incrementer(Table $table, Fluent $column) + { + if ($column->type == 'integer' and $column->increment) + { + return ' IDENTITY PRIMARY KEY'; + } + } + + /** + * Generate the SQL statement for creating a primary key. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function primary(Table $table, Fluent $command) + { + $name = $command->name; + + $columns = $this->columnize($columns); + + return 'ALTER TABLE '.$this->wrap($table)." ADD CONSTRAINT {$name} PRIMARY KEY ({$columns})"; + } + + /** + * Generate the SQL statement for creating a unique index. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function unique(Table $table, Fluent $command) + { + return $this->key($table, $command, true); + } + + /** + * Generate the SQL statement for creating a full-text index. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function fulltext(Table $table, Fluent $command) + { + $columns = $this->columnize($command->columns); + + // SQL Server requires the creation of a full-text "catalog" before + // creating a full-text index, so we'll first create the catalog + // then add another statement for the index. The catalog will + // be updated automatically by the server. + $sql[] = "CREATE FULLTEXT CATALOG {$command->catalog}"; + + $create = "CREATE FULLTEXT INDEX ON ".$this->wrap($table)." ({$columns}) "; + + // Full-text indexes must specify a unique, non-nullable column as + // the index "key" and this should have been created manually by + // the developer in a separate column addition command, so we + // can just specify it in this statement. + $sql[] = $create .= "KEY INDEX {$command->key} ON {$command->catalog}"; + + return $sql; + } + + /** + * Generate the SQL statement for creating a regular index. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function index(Table $table, Fluent $command) + { + return $this->key($table, $command); + } + + /** + * Generate the SQL statement for creating a new index. + * + * @param Table $table + * @param Fluent $command + * @param bool $unique + * @return string + */ + protected function key(Table $table, Fluent $command, $unique = false) + { + $columns = $this->columnize($command->columns); + + $create = ($unique) ? 'CREATE UNIQUE' : 'CREATE'; + + return $create." INDEX {$command->name} ON ".$this->wrap($table)." ({$columns})"; + } + + /** + * Generate the SQL statement for a drop table command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function drop(Table $table, Fluent $command) + { + return 'DROP TABLE '.$this->wrap($table); + } + + /** + * Generate the SQL statement for a drop column command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function drop_column(Table $table, Fluent $command) + { + $columns = array_map(array($this, 'wrap'), $command->columns); + + // Once we the array of column names, we need to add "drop" to the + // front of each column, then we'll concatenate the columns using + // commas and generate the alter statement SQL. + $columns = implode(', ', array_map(function($column) + { + return 'DROP '.$column; + + }, $columns)); + + return 'ALTER TABLE '.$this->wrap($table).' '.$columns; + } + + /** + * Generate the SQL statement for a drop primary key command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function drop_primary(Table $table, Fluent $command) + { + return 'ALTER TABLE '.$this->wrap($table).' DROP CONSTRAINT '.$command->name; + } + + /** + * Generate the SQL statement for a drop unqiue key command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function drop_unique(Table $table, Fluent $command) + { + return $this->drop_key($table, $command); + } + + /** + * Generate the SQL statement for a drop full-text key command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function drop_fulltext(Table $table, Fluent $command) + { + $sql[] = "DROP FULLTEXT INDEX ".$command->name; + + $sql[] = "DROP FULLTEXT CATALOG ".$command->catalog; + + return $sql; + } + + /** + * Generate the SQL statement for a drop index command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + public function drop_index(Table $table, Fluent $command) + { + return $this->drop_key($table, $command); + } + + /** + * Generate the SQL statement for a drop key command. + * + * @param Table $table + * @param Fluent $command + * @return string + */ + protected function drop_key(Table $table, Fluent $command) + { + return "DROP INDEX {$command->name} ON ".$this->wrap($table); + } + + /** + * Generate the data-type definition for a string. + * + * @param Fluent $column + * @return string + */ + protected function type_string(Fluent $column) + { + return 'NVARCHAR('.$column->length.')'; + } + + /** + * Generate the data-type definition for an integer. + * + * @param Fluent $column + * @return string + */ + protected function type_integer(Fluent $column) + { + return 'INT'; + } + + /** + * Generate the data-type definition for an integer. + * + * @param Fluent $column + * @return string + */ + protected function type_float(Fluent $column) + { + return 'FLOAT'; + } + + /** + * Generate the data-type definition for a boolean. + * + * @param Fluent $column + * @return string + */ + protected function type_boolean(Fluent $column) + { + return 'TINYINT'; + } + + /** + * Generate the data-type definition for a date. + * + * @param Fluent $column + * @return string + */ + protected function type_date(Fluent $column) + { + return 'DATETIME'; + } + + /** + * Generate the data-type definition for a timestamp. + * + * @param Fluent $column + * @return string + */ + protected function type_timestamp(Fluent $column) + { + return 'TIMESTAMP'; + } + + /** + * Generate the data-type definition for a text column. + * + * @param Fluent $column + * @return string + */ + protected function type_text(Fluent $column) + { + return 'NVARCHAR(MAX)'; + } + + /** + * Generate the data-type definition for a blob. + * + * @param Fluent $column + * @return string + */ + protected function type_blob(Fluent $column) + { + return 'VARBINARY(MAX)'; + } + +} \ No newline at end of file diff --git a/laravel/database/schema/table.php b/laravel/database/schema/table.php new file mode 100644 index 00000000..ef588833 --- /dev/null +++ b/laravel/database/schema/table.php @@ -0,0 +1,371 @@ +name = $name; + } + + /** + * Indicate that the table should be created. + * + * @return Fluent + */ + public function create() + { + return $this->command(__FUNCTION__); + } + + /** + * Create a new primary key on the table. + * + * @param string|array $columns + * @param string $name + * @return Fluent + */ + public function primary($columns, $name = null) + { + return $this->key(__FUNCTION__, $columns, $name); + } + + /** + * Create a new unique index on the table. + * + * @param string|array $columns + * @param string $name + * @return Fluent + */ + public function unique($columns, $name = null) + { + return $this->key(__FUNCTION__, $columns, $name); + } + + /** + * Create a new full-text index on the table. + * + * @param string|array $columns + * @param string $name + * @return Fluent + */ + public function fulltext($columns, $name = null) + { + return $this->key(__FUNCTION__, $columns, $name); + } + + /** + * Create a new index on the table. + * + * @param string|array + */ + public function index($columns, $name = null) + { + return $this->key(__FUNCTION__, $columns, $name); + } + + /** + * Create a command for creating any index. + * + * @param string $type + * @param string|array $columns + * @param string $name + * @return Fluent + */ + public function key($type, $columns, $name = null) + { + $parameters = array('name' => $name, 'columns' => (array) $columns); + + return $this->command($type, $parameters); + } + + /** + * Drop the database table. + * + * @return Fluent + */ + public function drop() + { + return $this->command(__FUNCTION__); + } + + /** + * Drop a column from the table. + * + * @param string|array $columns + * @return void + */ + public function drop_column($columns) + { + return $this->command(__FUNCTION__, array('columns' => (array) $columns)); + } + + /** + * Drop a primary key from the table. + * + * @param string $name + * @return void + */ + public function drop_primary($name) + { + return $this->drop_key(__FUNCTION__, $name); + } + + /** + * Drop a unique index from the table. + * + * @param string $name + * @return void + */ + public function drop_unique($name) + { + return $this->drop_key(__FUNCTION__, $name); + } + + /** + * Drop a full-text index from the table. + * + * @param string $name + * @return void + */ + public function drop_fulltext($name) + { + return $this->drop_key(__FUNCTION__, $name); + } + + /** + * Drop an index from the table. + * + * @param string $name + * @return void + */ + public function drop_index($name) + { + return $this->drop_key(__FUNCTION__, $name); + } + + /** + * Create a command to drop any type of index. + * + * @param string $type + * @param string $name + * @return Fluent + */ + protected function drop_key($type, $name) + { + return $this->command($type, array('name' => $name)); + } + + /** + * Add an auto-incrementing integer to the table. + * + * @param string $name + * @return Fluent + */ + public function increments($name) + { + return $this->integer($name, true); + } + + /** + * Add a string column to the table. + * + * @param string $name + * @param int $length + * @return Fluent + */ + public function string($name, $length = 200) + { + return $this->column(__FUNCTION__, compact('name', 'length')); + } + + /** + * Add an integer column to the table. + * + * @param string $name + * @param bool $increment + * @return Fluent + */ + public function integer($name, $increment = false) + { + return $this->column(__FUNCTION__, compact('name', 'increment')); + } + + /** + * Add a float column to the table. + * + * @param string $name + * @param bool $increment + * @return Fluent + */ + public function float($name) + { + return $this->column(__FUNCTION__, compact('name')); + } + + /** + * Add a boolean column to the table. + * + * @param string $name + * @return Fluent + */ + public function boolean($name) + { + return $this->column(__FUNCTION__, compact('name')); + } + + /** + * Create date-time columns for creation and update timestamps. + * + * @return void + */ + public function timestamps() + { + $this->date('created_at'); + + $this->date('updated_at'); + } + + /** + * Add a date-time column to the table. + * + * @param string $name + * @return Fluent + */ + public function date($name) + { + return $this->column(__FUNCTION__, compact('name')); + } + + /** + * Add a timestamp column to the table. + * + * @param string $name + * @return Fluent + */ + public function timestamp($name) + { + return $this->column(__FUNCTION__, compact('name')); + } + + /** + * Add a text column to the table. + * + * @param string $name + * @return Fluent + */ + public function text($name) + { + return $this->column(__FUNCTION__, compact('name')); + } + + /** + * Add a blob column to the table. + * + * @param string $name + * @return Fluent + */ + public function blob($name) + { + return $this->column(__FUNCTION__, compact('name')); + } + + /** + * Set the database connection for the table operation. + * + * @param string $connection + * @return void + */ + public function on($connection) + { + $this->connection = $connection; + } + + /** + * Determine if the schema table has a creation command. + * + * @return bool + */ + public function creating() + { + return ! is_null(array_first($this->commands, function($key, $value) + { + return $value->type == 'create'; + })); + } + + /** + * Create a new fluent command instance. + * + * @param string $type + * @param array $parameters + * @return Fluent + */ + protected function command($type, $parameters = array()) + { + $parameters = array_merge(compact('type'), $parameters); + + $this->commands[] = new Fluent($parameters); + + return end($this->commands); + } + + /** + * Create a new fluent column instance. + * + * @param string $type + * @param array $parameters + * @return Fluent + */ + protected function column($type, $parameters = array()) + { + $parameters = array_merge(compact('type'), $parameters); + + $this->columns[] = new Fluent($parameters); + + return end($this->columns); + } + +} \ No newline at end of file diff --git a/laravel/error.php b/laravel/error.php new file mode 100644 index 00000000..d82a7d13 --- /dev/null +++ b/laravel/error.php @@ -0,0 +1,96 @@ +

Unhandled Exception

+

Message:

+
".$exception->getMessage()."
+

Location:

+
".$exception->getFile()." on line ".$exception->getLine()."
+

Stack Trace:

+
".$exception->getTraceAsString()."
"; + } + else + { + Response::error('500')->send(); + } + + exit(1); + } + + /** + * Handle a native PHP error as an ErrorException. + * + * @param int $code + * @param string $error + * @param string $file + * @param int $line + * @return void + */ + public static function native($code, $error, $file, $line) + { + if (error_reporting() === 0) return; + + // For a PHP error, we'll create an ErrorExcepetion and then feed that + // exception to the exception method, which will create a simple view + // of the exception details. The ErrorException class is built-in to + // PHP for converting native errors to Exceptions. + $exception = new \ErrorException($error, $code, 0, $file, $line); + + if (in_array($code, Config::get('error.ignore'))) + { + return static::log($exception); + } + + static::exception($exception); + } + + /** + * Handle the PHP shutdown event. + * + * @return void + */ + public static function shutdown() + { + // If a fatal error occured that we have not handled yet, we will + // create an ErrorException and feed it to the exception handler, + // as it will not have been handled by the error handler. + if ( ! is_null($error = error_get_last())) + { + extract($error, EXTR_SKIP); + + static::exception(new \ErrorException($message, $type, 0, $file, $line)); + } + } + + /** + * Log an exception. + * + * @param Exception $exception + * @return void + */ + public static function log($exception) + { + if (Config::get('error.log')) + { + call_user_func(Config::get('error.logger'), $exception); + } + } + +} \ No newline at end of file diff --git a/laravel/event.php b/laravel/event.php new file mode 100644 index 00000000..10190e8b --- /dev/null +++ b/laravel/event.php @@ -0,0 +1,73 @@ + + * // Register a callback for the "start" event + * Event::listen('start', function() {return 'Started!';}); + * + * // Register an object instance callback for the given event + * Event::listen('event', array($object, 'method')); + * + * + * @param string $event + * @param mixed $callback + * @return void + */ + public static function listen($event, $callback) + { + static::$events[$event][] = $callback; + } + + /** + * Fire an event so that all listeners are called. + * + * + * // Fire the "start" event + * $responses = Event::fire('start'); + * + * // Fire the "start" event passing an array of parameters + * $responses = Event::fire('start', array('Laravel', 'Framework')); + * + * + * @param string $event + * @param array $parameters + * @return array + */ + public static function fire($event, $parameters = array()) + { + $responses = array(); + + if (static::listeners($event)) + { + foreach (static::$events[$event] as $callback) + { + $responses[] = call_user_func_array($callback, $parameters); + } + } + + return $responses; + } + +} \ No newline at end of file diff --git a/laravel/facades.php b/laravel/facades.php deleted file mode 100644 index f8dc7d13..00000000 --- a/laravel/facades.php +++ /dev/null @@ -1,66 +0,0 @@ - 5) - { - return call_user_func_array(array($class, $method), $parameters); - } - elseif ($count == 0) - { - return $class->$method(); - } - elseif ($count == 1) - { - return $class->$method($parameters[0]); - } - elseif ($count == 2) - { - return $class->$method($parameters[0], $parameters[1]); - } - elseif ($count == 3) - { - return $class->$method($parameters[0], $parameters[1], $parameters[2]); - } - elseif ($count == 4) - { - return $class->$method($parameters[0], $parameters[1], $parameters[2], $parameters[3]); - } - elseif ($count == 5) - { - return $class->$method($parameters[0], $parameters[1], $parameters[2], $parameters[3], $parameters[4]); - } - } - -} - -class Session extends Facade { public static $resolve = 'laravel.session'; } \ No newline at end of file diff --git a/laravel/file.php b/laravel/file.php index 8c1e9e37..a1e7a724 100644 --- a/laravel/file.php +++ b/laravel/file.php @@ -30,12 +30,7 @@ public static function exists($path) */ public static function get($path, $default = null) { - if (file_exists($path)) - { - return file_get_contents($path); - } - - return ($default instanceof Closure) ? call_user_func($default) : $default; + return (file_exists($path)) ? file_get_contents($path) : value($default); } /** @@ -117,25 +112,6 @@ public static function modified($path) return filemtime($path); } - /** - * Move an uploaded file to permanent storage. - * - * - * // Upload the $_FILES['photo'] file to a permanent location - * File::upload('photo', 'path/to/new/home.jpg'); - * - * - * @param string $key - * @param string $path - * @return bool - */ - public static function upload($key, $path) - { - if ( ! isset($_FILES[$key])) return false; - - return move_uploaded_file($_FILES[$key]['tmp_name'], $path); - } - /** * Get a file MIME type by extension. * @@ -163,7 +139,7 @@ public static function mime($extension, $default = 'application/octet-stream') /** * Determine if a file is a given type. * - * The Fileinfo PHP extension will be used to determine the MIME type of the file. + * The Fileinfo PHP extension is used to determine the file's MIME type. * * * // Determine if a file is a JPG image diff --git a/laravel/fluent.php b/laravel/fluent.php new file mode 100644 index 00000000..255ecc75 --- /dev/null +++ b/laravel/fluent.php @@ -0,0 +1,80 @@ + + * Create a new fluent container with attributes + * $fluent = new Fluent(array('name' => 'Taylor')); + * + * + * @param array $attributes + * @return void + */ + public function __construct($attributes = array()) + { + foreach ($attributes as $key => $value) + { + $this->$key = $value; + } + } + + /** + * Get an attribute from the fluent container. + * + * @param string $attribute + * @param mixed $default + * @return mixed + */ + public function get($attribute, $default = null) + { + return array_get($attributes, $attribute, $default); + } + + /** + * Handle dynamic calls to the container to set attributes. + * + * + * // Fluently set the value of a few attributes + * $fluent->name('Taylor')->age(25); + * + * // Set the value of an attribute to true (boolean) + * $fluent->nullable()->name('Taylor'); + * + */ + public function __call($method, $parameters) + { + $this->$method = (count($parameters) > 0) ? $parameters[0] : true; + + return $this; + } + + /** + * Dynamically retrieve the value of an attribute. + */ + public function __get($key) + { + if (array_key_exists($key, $this->attributes)) + { + return $this->attributes[$key]; + } + } + + /** + * Dynamically set the value of an attribute. + */ + public function __set($key, $value) + { + $this->attributes[$key] = $value; + } + +} \ No newline at end of file diff --git a/laravel/form.php b/laravel/form.php index df2b06d4..c67a8a25 100644 --- a/laravel/form.php +++ b/laravel/form.php @@ -1,15 +1,10 @@ * // Open a "POST" form to the current request URI * echo Form::open(); @@ -51,6 +40,9 @@ public static function open($action = null, $method = 'POST', $attributes = arra $attributes['action'] = static::action($action, $https); + // If a character encoding has not been specified in the attributes, we will + // use the default encoding as specified in the application configuration + // file for the "accept-charset" attribute. if ( ! array_key_exists('accept-charset', $attributes)) { $attributes['accept-charset'] = Config::get('application.encoding'); @@ -58,6 +50,10 @@ public static function open($action = null, $method = 'POST', $attributes = arra $append = ''; + // Since PUT and DELETE methods are not actually supported by HTML forms, + // we'll create a hidden input element that contains the request method + // and set the actual request method to POST. Laravel will look for the + // hidden element to determine the request method. if ($method == 'PUT' or $method == 'DELETE') { $append = static::hidden(Request::spoofer, $method); @@ -69,9 +65,6 @@ public static function open($action = null, $method = 'POST', $attributes = arra /** * Determine the appropriate request method to use for a form. * - * Since PUT and DELETE requests are spoofed using POST requests, we will substitute - * POST for any PUT or DELETE methods. Otherwise, the specified method will be used. - * * @param string $method * @return string */ @@ -155,7 +148,7 @@ public static function close() */ public static function token() { - return static::input('hidden', Session::csrf_token, IoC::core('session')->token()); + return static::input('hidden', Session::csrf_token, Session::token()); } /** @@ -185,9 +178,6 @@ public static function label($name, $value, $attributes = array()) /** * Create a HTML input element. * - * If an ID attribute is not specified and a label has been generated matching the input - * element name, the label name will be used as the element ID. - * * * // Create a "text" input element named "email" * echo Form::input('text', 'email'); @@ -490,8 +480,6 @@ public static function reset($value, $attributes = array()) /** * Create a HTML image input element. * - * The URL::to_asset method will be called on the given URL. - * * * // Create an image input element * echo Form::image('img/submit.png'); @@ -524,15 +512,16 @@ public static function button($value, $attributes = array()) /** * Determine the ID attribute for a form element. * - * An explicitly specified ID in the attributes takes first precedence, then - * the label names will be checked for a label matching the element name. - * * @param string $name * @param array $attributes * @return mixed */ protected static function id($name, $attributes) { + // If an ID has been explicitly specified in the attributes, we will + // use that ID. Otherwise, we will look for an ID in the array of + // label names as this makes it convenient to give input elements + // the same ID as their corresponding labels. if (array_key_exists('id', $attributes)) { return $attributes['id']; diff --git a/laravel/hash.php b/laravel/hash.php index 5c978b75..af12e20a 100644 --- a/laravel/hash.php +++ b/laravel/hash.php @@ -5,11 +5,6 @@ class Hash { /** * Hash a password using the Bcrypt hashing scheme. * - * Bcrypt provides a future-proof hashing algorithm by allowing the number of - * "rounds" to be increased, thus increasing the time it takes to generate the - * hashed value. The longer it takes takes to generate the hash, the more - * impractical a rainbow table attack against the hashes becomes. - * * * // Create a Bcrypt hash of a value * $hash = Hash::make('secret'); @@ -24,7 +19,9 @@ class Hash { */ public static function make($value, $rounds = 8) { - return crypt($value, '$2a$'.str_pad($rounds, 2, '0', STR_PAD_LEFT).'$'.static::salt()); + $work = str_pad($rounds, 2, '0', STR_PAD_LEFT); + + return crypt($value, '$2a$'.$work.'$'.static::salt()); } /** @@ -46,10 +43,10 @@ public static function check($value, $hash) */ protected static function salt() { - // Bcrypt expects the salt to be 22 base64 encoded characters, including dots - // and slashes. We will get rid of the plus signs included in the base64 data - // and replace them with dots. OpenSSL will be used if available, since it is - // more random, otherwise we will fallback on Str::random. + // Bcrypt expects the salt to be 22 base64 encoded characters including + // dots and slashes. We will get rid of the plus signs included in the + // base64 data and replace them with dots. OpenSSL will be used if it + // is available, otherwise we will use the Str::random method. if (function_exists('openssl_random_pseudo_bytes')) { $bytes = openssl_random_pseudo_bytes(16); @@ -57,7 +54,9 @@ protected static function salt() return substr(strtr(base64_encode($bytes), '+', '.'), 0 , 22); } - return substr(str_replace('+', '.', base64_encode(Str::random(40))), 0, 22); + $salt = str_replace('+', '.', base64_encode(Str::random(40))); + + return substr($salt, 0, 22); } } \ No newline at end of file diff --git a/laravel/helpers.php b/laravel/helpers.php index c37a9840..e1abaca3 100644 --- a/laravel/helpers.php +++ b/laravel/helpers.php @@ -24,4 +24,287 @@ function e($value) function __($key, $replacements = array(), $language = null) { return Laravel\Lang::line($key, $replacements, $language); +} + +/** + * Get an item from an array using "dot" notation. + * + * + * // Get the $array['user']['name'] value from the array + * $name = array_get($array, 'user.name'); + * + * // Return a default from if the specified item doesn't exist + * $name = array_get($array, 'user.name', 'Taylor'); + * + * + * @param array $array + * @param string $key + * @param mixed $default + * @return mixed + */ +function array_get($array, $key, $default = null) +{ + if (is_null($key)) return $array; + + foreach (explode('.', $key) as $segment) + { + if ( ! is_array($array) or ! array_key_exists($segment, $array)) + { + return value($default); + } + + $array = $array[$segment]; + } + + return $array; +} + +/** + * Set an array item to a given value using "dot" notation. + * + * If no key is given to the method, the entire array will be replaced. + * + * + * // Set the $array['user']['name'] value on the array + * array_set($array, 'user.name', 'Taylor'); + * + * // Set the $array['user']['name']['first'] value on the array + * array_set($array, 'user.name.first', 'Michael'); + * + * + * @param array $array + * @param string $key + * @param mixed $value + * @return void + */ +function array_set(&$array, $key, $value) +{ + if (is_null($key)) return $array = $value; + + $keys = explode('.', $key); + + // This loop allows us to dig down into the array to a dynamic depth by + // setting the array value for each level that we dig into. Once there + // is one key left, we can fall out of the loop and set the value as + // we should be at the proper depth within the array. + while (count($keys) > 1) + { + $key = array_shift($keys); + + // If the key doesn't exist at this depth, we will just create an + // empty array to hold the next value, allowing us to create the + // arrays necessary to hold the final value at the proper depth. + if ( ! isset($array[$key]) or ! is_array($array[$key])) + { + $array[$key] = array(); + } + + $array =& $array[$key]; + } + + $array[array_shift($keys)] = $value; +} + +/** + * Remove an array item from a given array using "dot" notation. + * + * + * // Remove the $array['user']['name'] item from the array + * array_forget($array, 'user.name'); + * + * // Remove the $array['user']['name']['first'] item from the array + * array_forget($array, 'user.name.first'); + * + * + * @param array $array + * @param string $key + * @return void + */ +function array_forget(&$array, $key) +{ + $keys = explode('.', $key); + + // This loop functions very similarly to the loop in the "set" method. + // We will iterate over the keys, setting the array value to the new + // depth at each iteration. Once there is only one key left, we will + // be at the proper depth in the array to "forget" the value. + while (count($keys) > 1) + { + $key = array_shift($keys); + + // Since this method is supposed to remove a value from the array, + // if a value higher up in the chain doesn't exist, there is no + // need to keep digging into the array, since it is impossible + // for the final value to even exist in the array. + if ( ! isset($array[$key]) or ! is_array($array[$key])) + { + return; + } + + $array =& $array[$key]; + } + + unset($array[array_shift($keys)]); +} + +/** + * Return the first element in an array which passes a given truth test. + * + * + * // Return the first array element that equals "Taylor" + * $value = array_first($array, function($k, $v) {return $v == 'Taylor';}); + * + * // Return a default value if no matching element is found + * $value = array_first($array, function($k, $v) {return $v == 'Taylor'}, 'Default'); + * + * + * @param array $array + * @param Closure $callback + * @param mixed $default + * @return mixed + */ +function array_first($array, $callback, $default = null) +{ + foreach ($array as $key => $value) + { + if (call_user_func($callback, $key, $value)) return $value; + } + + return value($default); +} + +/** + * Spin through the array, executing a callback with each key and element. + * + * @param array $array + * @param mixed $callback + * @return array + */ +function array_spin($array, $callback) +{ + return array_map($callback, array_keys($array), array_values($array)); +} + +/** + * Return the first element of an array. + * + * This is simply a convenient wrapper around the "reset" method. + * + * @param array $array + * @return mixed + */ +function head($array) +{ + return reset($array); +} + +/** + * Generate an application URL. + * + * + * // Create a URL to a location within the application + * $url = path('user/profile'); + * + * // Create a HTTPS URL to a location within the application + * $url = path('user/profile', true); + * + * + * @param string $url + * @param bool $https + * @return string + */ +function path($url = '', $https = false) +{ + return Laravel\URL::to($url, $https); +} + +/** + * Generate an application URL to an asset. + * + * @param string $url + * @param bool $https + * @return string + */ +function asset($url, $https = false) +{ + return Laravel\URL::to_asset($url, $https); +} + +/** + * Generate a URL to a controller action. + * + * + * // Generate a URL to the "index" method of the "user" controller + * $url = action('user@index'); + * + * // Generate a URL to http://example.com/user/profile/taylor + * $url = action('user@profile', array('taylor')); + * + * + * @param string $action + * @param array $parameters + * @param bool $https + * @return string + */ +function action($action, $parameters = array(), $https = false) +{ + return Laravel\URL::to_action($action, $parameters, $https); +} + +/** + * Generate a URL from a route name. + * + * + * // Create a URL to the "profile" named route + * $url = route('profile'); + * + * // Create a URL to the "profile" named route with wildcard parameters + * $url = route('profile', array($username)); + * + * + * @param string $name + * @param array $parameters + * @param bool $https + * @return string + */ +function route($name, $parameters = array(), $https = false) +{ + return Laravel\URL::to_route($name, $parameters, $https); +} + +/** + * Determine if a given string begins with a given value. + * + * @param string $haystack + * @param string $needle + * @return bool + */ +function starts_with($haystack, $needle) +{ + return strpos($haystack, $needle) === 0; +} + +/** + * Determine if a given string contains a given sub-string. + * + * @param string $haystack + * @param string $needle + * @return bool + */ +function str_contains($haystack, $needle) +{ + return strpos($haystack, $needle) !== false; +} + +/** + * Return the value of the given item. + * + * If the given item is a Closure the result of the Closure will be returned. + * + * @param mixed $value + * @return mixed + */ +function value($value) +{ + return ($value instanceof Closure) ? call_user_func($value) : $value; } \ No newline at end of file diff --git a/laravel/html.php b/laravel/html.php index 19f17699..1256d5f3 100644 --- a/laravel/html.php +++ b/laravel/html.php @@ -12,7 +12,7 @@ class HTML { */ public static function entities($value) { - return htmlentities($value, ENT_QUOTES, Config::$items['application']['encoding'], false); + return htmlentities($value, ENT_QUOTES, Config::get('application.encoding'), false); } /** @@ -58,13 +58,7 @@ public static function style($url, $attributes = array()) { $defaults = array('media' => 'all', 'type' => 'text/css', 'rel' => 'stylesheet'); - foreach ($defaults as $attribute => $default) - { - if ( ! array_key_exists($attribute, $attributes)) - { - $attributes[$attribute] = $default; - } - } + $attributes = $attributes + $defaults; $url = static::entities(URL::to_asset($url)); @@ -274,6 +268,9 @@ private static function listing($type, $list, $attributes = array()) foreach ($list as $key => $value) { + // If the value is an array, we will recurse the function so that we can + // produce a nested list within the list being built. Of course, nested + // lists may exist within nested lists, etc. if (is_array($value)) { $html .= static::listing($type, $value); @@ -290,9 +287,6 @@ private static function listing($type, $list, $attributes = array()) /** * Build a list of HTML attributes from an array. * - * Numeric-keyed attributes will be assigned the same key and value to handle - * attributes such as "autofocus" and "required". - * * @param array $attributes * @return string */ @@ -302,6 +296,9 @@ public static function attributes($attributes) foreach ((array) $attributes as $key => $value) { + // For numeric keys, we will assume that the key and the value are the + // same, as this will conver HTML attributes such as "required" that + // may be specified as required="required". if (is_numeric($key)) $key = $value; if ( ! is_null($value)) @@ -325,19 +322,20 @@ protected static function obfuscate($value) foreach (str_split($value) as $letter) { + // To properly obfuscate the value, we will randomly convert each + // letter to its entity or hexadecimal representation, keeping a + // bot from sniffing the randomly obfuscated letters from the + // page and guarding against e-mail harvesting. switch (rand(1, 3)) { - // Convert the letter to its entity representation. case 1: $safe .= '&#'.ord($letter).';'; break; - // Convert the letter to a Hex character code. case 2: $safe .= '&#x'.dechex(ord($letter)).';'; break; - // No encoding. case 3: $safe .= $letter; } @@ -346,39 +344,4 @@ protected static function obfuscate($value) return $safe; } - /** - * Magic Method for handling dynamic static methods. - * - * This method primarily handles dynamic calls to create links to named routes. - * - * - * // Generate a link to the "profile" named route - * echo HTML::link_to_profile('Profile'); - * - * // Generate a link to the "profile" route and add some parameters - * echo HTML::link_to_profile('Profile', array('taylor')); - * - * // Generate a link to the "profile" named route using HTTPS - * echo HTML::link_to_secure_profile('Profile'); - * - */ - public static function __callStatic($method, $parameters) - { - if (strpos($method, 'link_to_secure_') === 0) - { - array_unshift($parameters, substr($method, 15)); - - return forward_static_call_array('HTML::link_to_secure_route', $parameters); - } - - if (strpos($method, 'link_to_') === 0) - { - array_unshift($parameters, substr($method, 8)); - - return forward_static_call_array('HTML::link_to_route', $parameters); - } - - throw new \BadMethodCallException("Method [$method] is not defined on the HTML class."); - } - } diff --git a/laravel/inflector.php b/laravel/inflector.php deleted file mode 100644 index a3573fb7..00000000 --- a/laravel/inflector.php +++ /dev/null @@ -1,205 +0,0 @@ - "$1zes", - '/^(ox)$/i' => "$1en", - '/([m|l])ouse$/i' => "$1ice", - '/(matr|vert|ind)ix|ex$/i' => "$1ices", - '/(x|ch|ss|sh)$/i' => "$1es", - '/([^aeiouy]|qu)y$/i' => "$1ies", - '/(hive)$/i' => "$1s", - '/(?:([^f])fe|([lr])f)$/i' => "$1$2ves", - '/(shea|lea|loa|thie)f$/i' => "$1ves", - '/sis$/i' => "ses", - '/([ti])um$/i' => "$1a", - '/(tomat|potat|ech|her|vet)o$/i' => "$1oes", - '/(bu)s$/i' => "$1ses", - '/(alias)$/i' => "$1es", - '/(octop)us$/i' => "$1i", - '/(ax|test)is$/i' => "$1es", - '/(us)$/i' => "$1es", - '/s$/i' => "s", - '/$/' => "s" - ); - - /** - * Singular word forms. - * - * @var array - */ - private static $singular = array( - '/(quiz)zes$/i' => "$1", - '/(matr)ices$/i' => "$1ix", - '/(vert|ind)ices$/i' => "$1ex", - '/^(ox)en$/i' => "$1", - '/(alias)es$/i' => "$1", - '/(octop|vir)i$/i' => "$1us", - '/(cris|ax|test)es$/i' => "$1is", - '/(shoe)s$/i' => "$1", - '/(o)es$/i' => "$1", - '/(bus)es$/i' => "$1", - '/([m|l])ice$/i' => "$1ouse", - '/(x|ch|ss|sh)es$/i' => "$1", - '/(m)ovies$/i' => "$1ovie", - '/(s)eries$/i' => "$1eries", - '/([^aeiouy]|qu)ies$/i' => "$1y", - '/([lr])ves$/i' => "$1f", - '/(tive)s$/i' => "$1", - '/(hive)s$/i' => "$1", - '/(li|wi|kni)ves$/i' => "$1fe", - '/(shea|loa|lea|thie)ves$/i' => "$1f", - '/(^analy)ses$/i' => "$1sis", - '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => "$1$2sis", - '/([ti])a$/i' => "$1um", - '/(n)ews$/i' => "$1ews", - '/(h|bl)ouses$/i' => "$1ouse", - '/(corpse)s$/i' => "$1", - '/(us)es$/i' => "$1", - '/(us|ss)$/i' => "$1", - '/s$/i' => "", - ); - - /** - * Irregular word forms. - * - * @var array - */ - private static $irregular = array( - 'child' => 'children', - 'foot' => 'feet', - 'goose' => 'geese', - 'man' => 'men', - 'move' => 'moves', - 'person' => 'people', - 'sex' => 'sexes', - 'tooth' => 'teeth', - ); - - /** - * Uncountable word forms. - * - * @var array - */ - private static $uncountable = array( - 'audio', - 'equipment', - 'deer', - 'fish', - 'gold', - 'information', - 'money', - 'rice', - 'police', - 'series', - 'sheep', - 'species', - ); - - /** - * Convert a word to its plural form. - * - * Optionally, a count argument may be provided. If the count is greater than - * one, the word will be pluralized, otherwise the word will be returned from - * the method unchanged. - * - * - * // Get the plural form of the word "child" - * $children = Inflector::plural('child'); - * - * // Returns "comments" - * $comments = Inflector::plural('comment', 10); - * - * // Returns "comment" - * $comment = Inflector::plural('comment', 1); - * - * - * @param string $value - * @param int $count - * @return string - */ - public static function plural($value, $count = null) - { - if ( ! is_null($count) and $count == 1) return $value; - - $irregular = array_flip(static::$irregular); - - $plural = static::inflect($value, static::$plural_cache, $irregular, static::$plural); - - return static::$plural_cache[$value] = $plural; - } - - /** - * Convert a word to its singular form. - * - * @param string $value - * @return string - */ - public static function singular($value) - { - $singular = static::inflect($value, static::$singular_cache, static::$irregular, static::$singular); - - return static::$singular_cache[$value] = $singular; - } - - /** - * Convert a word to its singular or plural form. - * - * @param string $value - * @param array $cache - * @param array $irregular - * @param array $source - * @return string - */ - protected static function inflect($value, $cache, $irregular, $source) - { - if (array_key_exists($value, $cache)) - { - return $cache[$value]; - } - - if (in_array(strtolower($value), static::$uncountable)) - { - return $value; - } - - foreach ($irregular as $irregular => $pattern) - { - if (preg_match($pattern = '/'.$pattern.'$/i', $value)) - { - return preg_replace($pattern, $irregular, $value); - } - } - - foreach ($source as $pattern => $inflected) - { - if (preg_match($pattern, $value)) - { - return preg_replace($pattern, $inflected, $value); - } - } - - return $value; - } - -} \ No newline at end of file diff --git a/laravel/input.php b/laravel/input.php index 64290167..8524957e 100644 --- a/laravel/input.php +++ b/laravel/input.php @@ -17,9 +17,7 @@ class Input { const old_input = 'laravel_old_input'; /** - * Get all of the input data for the request. - * - * This method returns a merged array containing Input::get() and Input::files(). + * Get all of the input data for the request, including files. * * @return array */ @@ -31,7 +29,7 @@ public static function all() /** * Determine if the input data contains an item. * - * If the item is in the input array, but is an empty string, false will be returned. + * If the input item is an empty string, false will be returned. * * @param string $key * @return bool @@ -44,7 +42,7 @@ public static function has($key) /** * Get an item from the input data. * - * This method should be used for all request methods (GET, POST, PUT, and DELETE). + * This method is used for all request verbs (GET, POST, PUT, and DELETE). * * * // Get the "email" item from the input array @@ -60,56 +58,45 @@ public static function has($key) */ public static function get($key = null, $default = null) { - return Arr::get(static::$input, $key, $default); + return array_get(static::$input, $key, $default); } /** - * Flash the input for the current request to the session. - * - * The input data to be flashed may be controlled by using a filter and an array - * of included or excluded input data. This provides a convenient way of keeping - * sensitive information like passwords out of the session. + * Get a subset of the items from the input data. * * - * // Flash all of the input data to the session - * Input::flash(); + * // Get only the email from the input data + * $value = Input::only('email'); * - * // Flash only a few input items to the session - * Input::flash('only', array('name', 'email')); - * - * // Flash all but a few input items to the session - * Input::flash('except', array('password')); + * // Get only the username and email from the input data + * $input = Input::only(array('username', 'email')); * * - * @return void + * @param array $keys + * @return array */ - public static function flash($filter = null, $items = array()) + public static function only($keys) { - $flash = static::get(); - - // Since the items flashed to the session can be filtered, we will iterate - // all of the input data and either remove or include the input item based - // on the specified filter and array of items to be flashed. - if ($filter == 'only') - { - $flash = array_intersect_key($flash, array_flip($items)); - } - elseif ($filter == 'except') - { - $flash = array_diff_key($flash, array_flip($items)); - } - - IoC::core('session')->flash(Input::old_input, $flash); + return array_intersect_key(static::get(), array_flip((array) $keys)); } /** - * Flush the old input from the session. + * Get all of the input data except for a specified array of items. * - * @return void + * + * // Get all of the input data except for username + * $input = Input::except('username'); + * + * // Get all of the input data except for username and email + * $input = Input::except(array('username', 'email')); + * + * + * @param array $keys + * @return array */ - public static function flush() + public static function except($keys) { - IoC::core('session')->flash(Input::old_input, array()); + return array_diff_key(static::get(), array_flip($keys)); } /** @@ -140,9 +127,7 @@ public static function had($key) */ public static function old($key = null, $default = null) { - $old = IoC::core('session')->get(Input::old_input, array()); - - return Arr::get($old, $key, $default); + return array_get(Session::get(Input::old_input, array()), $key, $default); } /** @@ -152,7 +137,7 @@ public static function old($key = null, $default = null) * // Get the array of information for the "picture" upload * $picture = Input::file('picture'); * - * // Get a specific element from the file array + * // Get a specific element from within the file's data array * $size = Input::file('picture.size'); * * @@ -162,7 +147,7 @@ public static function old($key = null, $default = null) */ public static function file($key = null, $default = null) { - return Arr::get($_FILES, $key, $default); + return array_get($_FILES, $key, $default); } /** @@ -171,8 +156,8 @@ public static function file($key = null, $default = null) * This method is simply a convenient wrapper around move_uploaded_file. * * - * // Move the "picture" item from the $_FILES array to a permanent location - * Input::upload('picture', 'path/to/storage/picture.jpg'); + * // Move the "picture" file to a permanent location on disk + * Input::upload('picture', 'path/to/photos/picture.jpg'); * * * @param string $key @@ -181,7 +166,44 @@ public static function file($key = null, $default = null) */ public static function upload($key, $path) { - return File::upload($key, $path); + if (is_null(static::file($key))) return false; + + return move_uploaded_file(static::file("{$key}.tmp_name"), $path); + } + + /** + * Flash the input for the current request to the session. + * + * + * // Flash all of the input to the session + * Input::flash(); + * + * // Flash only a few input items to the session + * Input::flash('only', array('name', 'email')); + * + * // Flash all but a few input items to the session + * Input::flash('except', array('password', 'social_number')); + * + * + * @param string $filter + * @param array $keys + * @return void + */ + public static function flash($filter = null, $keys = array()) + { + $flash = ( ! is_null($filter)) ? static::$filter($keys) : static::get(); + + Session::flash(Input::old_input, $flash); + } + + /** + * Flush all of the old input from the session. + * + * @return void + */ + public static function flush() + { + Session::flash(Input::old_input, array()); } } \ No newline at end of file diff --git a/laravel/ioc.php b/laravel/ioc.php index 34e8f63c..4827d30f 100644 --- a/laravel/ioc.php +++ b/laravel/ioc.php @@ -1,4 +1,4 @@ - - * // Register an object and its resolver - * IoC::register('mailer', function($c) {return new Mailer;}); - * - * * @param string $name * @param Closure $resolver * @return void */ - public static function register($name, $resolver, $singleton = false) + public static function register($name, Closure $resolver, $singleton = false) { - static::$registry[$name] = array('resolver' => $resolver, 'singleton' => $singleton); + static::$registry[$name] = compact('resolver', 'singleton'); } /** @@ -67,7 +43,6 @@ public static function registered($name) * Register an object as a singleton. * * Singletons will only be instantiated the first time they are resolved. - * The same instance will be returned on subsequent requests. * * @param string $name * @param Closure $resolver @@ -79,10 +54,7 @@ public static function singleton($name, $resolver) } /** - * Register an instance as a singleton. - * - * This method allows you to register an already existing object instance - * with the container to be managed as a singleton instance. + * Register an existing instance as a singleton. * * * // Register an instance as a singleton in the container @@ -105,11 +77,8 @@ public static function instance($name, $instance) * // Resolve the "laravel.router" class from the container * $input = IoC::core('router'); * - * // Equivalent resolution using the "resolve" method + * // Equivalent resolution of the router using the "resolve" method * $input = IoC::resolve('laravel.router'); - * - * // Pass an array of parameters to the resolver - * $input = IoC::core('router', array('test')); * * * @param string $name @@ -128,7 +97,7 @@ public static function core($name, $parameters = array()) * // Get an instance of the "mailer" object registered in the container * $mailer = IoC::resolve('mailer'); * - * // Pass an array of parameters to the resolver + * // Get an instance of the "mailer" object and pass parameters to the resolver * $mailer = IoC::resolve('mailer', array('test')); * * @@ -145,12 +114,19 @@ public static function resolve($name, $parameters = array()) if ( ! static::registered($name)) { - throw new \OutOfBoundsException("Error resolving [$name]. No resolver has been registered."); + throw new \Exception("Error resolving [$name]. No resolver has been registered."); } $object = call_user_func(static::$registry[$name]['resolver'], $parameters); - if (isset(static::$registry[$name]['singleton']) and static::$registry[$name]['singleton']) + // If the resolver is registering as a singleton resolver, we will cache + // the instance of the object in the container so we can resolve it next + // time without having to instantiate a new instance of the object. + // + // This allows the developer to reuse objects that do not need to be + // instantiated each time they are needed, such as a SwiftMailer or + // Twig object that can be shared. + if (isset(static::$registry[$name]['singleton'])) { return static::$singletons[$name] = $object; } @@ -158,11 +134,4 @@ public static function resolve($name, $parameters = array()) return $object; } -} - -/** - * We only bootstrap the IoC container once the class has been - * loaded since there isn't any reason to load the container - * configuration until the class is first requested. - */ -IoC::bootstrap(); \ No newline at end of file +} \ No newline at end of file diff --git a/laravel/lang.php b/laravel/lang.php index 15344d0b..688a6eef 100644 --- a/laravel/lang.php +++ b/laravel/lang.php @@ -26,7 +26,7 @@ class Lang { /** * All of the loaded language lines. * - * The array is keyed by [$language][$file]. + * The array is keyed by [$bundle][$language][$file]. * * @var array */ @@ -54,6 +54,9 @@ protected function __construct($key, $replacements = array(), $language = null) * // Create a new language line instance for a given line * $line = Lang::line('validation.required'); * + * // Create a new language line for a line belonging to a bundle + * $line = Lang::line('admin::messages.welcome'); + * * // Specify some replacements for the language line * $line = Lang::line('validation.required', array('attribute' => 'email')); * @@ -65,10 +68,7 @@ protected function __construct($key, $replacements = array(), $language = null) */ public static function line($key, $replacements = array(), $language = null) { - if (is_null($language)) - { - $language = Config::$items['application']['language']; - } + if (is_null($language)) $language = Config::get('application.language'); return new static($key, $replacements, $language); } @@ -76,9 +76,6 @@ public static function line($key, $replacements = array(), $language = null) /** * Get the language line as a string. * - * If a language is specified, it should correspond to a directory - * within your application language directory. - * * * // Get a language line * $line = Lang::line('validation.required')->get(); @@ -98,92 +95,104 @@ public function get($language = null, $default = null) { if (is_null($language)) $language = $this->language; - list($file, $line) = $this->parse($this->key); + list($bundle, $file, $line) = $this->parse($this->key); - if ( ! $this->load($file)) + // If the file doesn't exist, we'll just return the default value that was + // given to the method. The default value is also returned even when the + // file exists and the file does not actually contain any lines. + if ( ! static::load($bundle, $language, $file)) { - return ($default instanceof Closure) ? call_user_func($default) : $default; + return value($default); } - return $this->replace(Arr::get(static::$lines[$language][$file], $line, $default)); - } + $lines = static::$lines[$bundle][$language][$file]; - /** - * Make all necessary replacements on a language line. - * - * Replacements place-holder are prefixed with a colon, and are replaced - * with the appropriate value based on the replacement array set for the - * language line instance. - * - * @param string $line - * @return string - */ - protected function replace($line) - { - foreach ($this->replacements as $key => $value) + $line = array_get($lines, $line, $default); + + // If the line is not a string, it probably means the developer asked for + // the entire langauge file and the value of the requested value will be + // an array containing all of the lines in the file. + if (is_string($line)) { - $line = str_replace(':'.$key, $value, $line); + foreach ($this->replacements as $key => $value) + { + $line = str_replace(':'.$key, $value, $line); + } } return $line; } /** - * Parse a language key into its file and line segments. + * Parse a language key into its bundle, file, and line segments. * - * Language keys are formatted similarly to configuration keys. The first - * segment represents the language file, while the second segment - * represents a language line within that file. + * Language lines follow a {bundle}::{file}.{line} naming convention. * * @param string $key * @return array */ protected function parse($key) { - if (count($segments = explode('.', $key)) > 1) - { - return array($segments[0], implode('.', array_slice($segments, 1))); - } + $bundle = Bundle::name($key); - throw new \InvalidArgumentException("Invalid language line [$key]."); + $segments = explode('.', Bundle::element($key)); + + // If there are not at least two segments in the array, it means that + // the developer is requesting the entire language line array to be + // returned. If that is the case, we'll make the item "null". + if (count($segments) >= 2) + { + $line = implode('.', array_slice($segments, 1)); + + return array($bundle, $segments[0], $line); + } + else + { + return array($bundle, $segments[0], null); + } } /** * Load all of the language lines from a language file. * - * If the language file is successfully loaded, true will be returned. - * + * @param string $bundle + * @param string $language * @param string $file * @return bool */ - protected function load($file) + public static function load($bundle, $language, $file) { - if (isset(static::$lines[$this->language][$file])) return true; - - $language = array(); - - if (file_exists($path = LANG_PATH.$this->language.'/'.$file.EXT)) + if (isset(static::$lines[$bundle][$language][$file])) { - $language = array_merge($language, require $path); + return true; } - // If language lines were actually found, they will be loaded into - // the array containing all of the lines for all languages and files. - // The array is keyed by the language and the file name. - if (count($language) > 0) - { - static::$lines[$this->language][$file] = $language; - } - - return isset(static::$lines[$this->language][$file]); + $lines = array(); + + // Language files can belongs to the application or to any bundle + // that is installed for the application. So, we'll need to use + // the bundle's path when checking for the file. + // + // This is similar to the loading method for configuration files, + // but we do not need to cascade across directories since most + // likely language files are static across environments. + $path = Bundle::path($bundle)."language/{$language}/{$file}".EXT; + + if (file_exists($path)) $lines = require $path; + + static::$lines[$bundle][$language][$file] = $lines; + + return count($lines) > 0; } /** * Get the string content of the language line. + * + * @return string */ public function __toString() { - return $this->get(); + return (string) $this->get(); } } \ No newline at end of file diff --git a/laravel/laravel.php b/laravel/laravel.php index 9ec22f36..0698360e 100644 --- a/laravel/laravel.php +++ b/laravel/laravel.php @@ -1,61 +1,26 @@

Unhandled Exception

-

Message:

-
".$exception->getMessage()."
-

Location:

-
".$exception->getFile()." on line ".$exception->getLine()."
-

Stack Trace:

-
".$exception->getTraceAsString()."
"; - } - else - { - Response::error('500')->send(); - } - - exit(1); -}; - +date_default_timezone_set(Config::get('application.timezone')); /** * Register the PHP exception handler. The framework throws exceptions * on every error that cannot be handled. All of those exceptions will * be sent through this closure for processing. */ -set_exception_handler(function($exception) use ($handler) +set_exception_handler(function($e) { - $handler($exception); + Error::exception($e); }); /** @@ -65,18 +30,9 @@ * errors are ignored and errors in the developer configured whitelist * are silently logged. */ -set_error_handler(function($code, $error, $file, $line) use ($logger) +set_error_handler(function($code, $error, $file, $line) { - if (error_reporting() === 0) return; - - $exception = new \ErrorException($error, $code, 0, $file, $line); - - if (in_array($code, Config::$items['error']['ignore'])) - { - return $logger($exception); - } - - throw $exception; + Error::native($code, $error, $file, $line); }); /** @@ -85,14 +41,9 @@ * has occured, we will convert it to an ErrorException and pass it * to the common exception handler for the framework. */ -register_shutdown_function(function() use ($handler) +register_shutdown_function(function() { - if ( ! is_null($error = error_get_last())) - { - extract($error, EXTR_SKIP); - - $handler(new \ErrorException($message, $type, 0, $file, $line)); - } + Error::shutdown(); }); /** @@ -114,21 +65,17 @@ * payload will be registered in the IoC container as an instance * so it can be retrieved easily throughout the application. */ -if (Config::$items['session']['driver'] !== '') +if (Config::get('session.driver') !== '') { - $driver = Session\Drivers\Factory::make(Config::$items['session']['driver']); + Session::start(Config::get('session.driver')); - $session = new Session\Payload($driver); - - $session->load(Cookie::get(Config::$items['session']['cookie'])); - - IoC::instance('laravel.session', $session); + Session::load(Cookie::get(Config::get('session.cookie'))); } /** * Gather the input to the application based on the current request. - * The input will be gathered based on the current request method and - * will be set on the Input manager. + * The input will be gathered based on the current request method + * and will be set on the Input manager. */ $input = array(); @@ -157,28 +104,55 @@ /** * The spoofed request method is removed from the input so it is not * unexpectedly included in Input::all() or Input::get(). Leaving it - * in the input array could cause unexpected results if the developer - * fills an Eloquent model with the input. + * in the input array could cause unexpected results if an Eloquent + * model is filled with the input. */ unset($input[Request::spoofer]); Input::$input = $input; +/** + * Start all of the bundles that are specified in the configuration + * array of auto-loaded bundles. This gives the developer the ability + * to conveniently and automatically load bundles that are used on + * every request to their application. + */ +foreach (Config::get('application.bundles') as $bundle) +{ + Bundle::start($bundle); +} + +/** + * Load the "application" bundle. Though the application folder is + * not typically considered a bundle, it is started like one and + * essentially serves as the "default" bundle. + */ +Bundle::start(DEFAULT_BUNDLE); + +/** + * If the first segment of the request URI corresponds with a bundle, + * we will start that bundle. By convention, bundles handle all URIs + * which begin with their bundle name. + */ +$bundle = URI::segment(1); + +if ( ! is_null($bundle) and Bundle::routable($bundle)) +{ + Bundle::start($bundle); +} + /** * Route the request to the proper route in the application. If a * route is found, the route will be called with the current request * instance. If no route is found, the 404 response will be returned * to the browser. */ -Routing\Filter::register(require APP_PATH.'filters'.EXT); +if (count(URI::$segments) > 15) +{ + throw new \Exception("Invalid request. Too many URI segments."); +} -$loader = new Routing\Loader(APP_PATH, ROUTE_PATH); - -$router = new Routing\Router($loader, CONTROLLER_PATH); - -IoC::instance('laravel.routing.router', $router); - -Request::$route = $router->route(Request::method(), URI::current()); +Request::$route = Routing\Router::route(Request::method(), URI::current()); if ( ! is_null(Request::$route)) { @@ -195,9 +169,9 @@ * driver is a sweeper, session garbage collection might be * performed depending on the "sweepage" probability. */ -if (Config::$items['session']['driver'] !== '') +if (Config::get('session.driver') !== '') { - IoC::core('session')->save(); + Session::save(); } $response->send(); \ No newline at end of file diff --git a/laravel/log.php b/laravel/log.php new file mode 100644 index 00000000..b2ef534a --- /dev/null +++ b/laravel/log.php @@ -0,0 +1,65 @@ +getMessage().' in '.$e->getFile().' on line '.$e->getLine(); + } + + /** + * Write a message to the log file. + * + * + * // Write an "error" messge to the log file + * Log::write('error', 'Something went horribly wrong!'); + * + * // Write an "error" message using the class' magic method + * Log::error('Something went horribly wrong!'); + * + * + * @param string $type + * @param string $message + * @return void + */ + public static function write($type, $message) + { + $message = date('Y-m-d H:i:s').' '.Str::upper($type)." - {$message}".PHP_EOL; + + File::append(STORAGE_PATH.'logs/'.date('Y-m').'.log', $message); + } + + /** + * Dynamically write a log message. + * + * + * // Write an "error" message to the log file + * Log::error('This is an error!'); + * + * // Write a "warning" message to the log file + * Log::warning('This is a warning!'); + * + */ + public static function __callStatic($method, $parameters) + { + static::write($method, $parameters[0]); + } + +} \ No newline at end of file diff --git a/laravel/memcached.php b/laravel/memcached.php index 6d8c8fd6..d073737f 100644 --- a/laravel/memcached.php +++ b/laravel/memcached.php @@ -7,42 +7,38 @@ class Memcached { * * @var Memcache */ - protected static $instance; + protected static $connection; /** * Get the Memcached connection instance. * - * This connection will be managed as a singleton instance so that only - * one connection to the Memcached severs will be established. + * + * // Get the Memcache connection and get an item from the cache + * $name = Memcached::connection()->get('name'); + * + * // Get the Memcache connection and place an item in the cache + * Memcached::connection()->set('name', 'Taylor'); + * * * @return Memcache */ - public static function instance() + public static function connection() { - if (is_null(static::$instance)) + if (is_null(static::$connection)) { - static::$instance = static::connect(Config::get('cache.memcached')); + static::$connection = static::connect(Config::get('cache.memcached')); } - return static::$instance; + return static::$connection; } /** * Create a new Memcached connection instance. * - * The configuration array passed to this method should be an array of - * server hosts / ports, like those defined in the cache configuration - * file. - * - * - * // Create a new localhost Memcached connection instance. - * $memcache = Memcached::connect(array('host' => '127.0.0.1', 'port' => 11211)); - * - * * @param array $servers * @return Memcache */ - public static function connect($servers) + protected static function connect($servers) { $memcache = new \Memcache; @@ -53,7 +49,7 @@ public static function connect($servers) if ($memcache->getVersion() === false) { - throw new \RuntimeException('Could not establish memcached connection.'); + throw new \Exception('Could not establish memcached connection.'); } return $memcache; diff --git a/laravel/messages.php b/laravel/messages.php index a2c1a0ce..4b499720 100644 --- a/laravel/messages.php +++ b/laravel/messages.php @@ -12,8 +12,6 @@ class Messages { /** * Create a new Messages instance. * - * The Messages class provides a convenient wrapper around an array of strings. - * * @return void */ public function __construct($messages = array()) @@ -62,14 +60,14 @@ public function has($key) } /** - * Get the first message for a given key. + * Get the first message from the container for a given key. * * - * // Get the first message for the e-mail attribute - * $email = $messages->first('email'); + * // Echo the first message for the e-mail attribute + * echo $messages->first('email'); * * // Format the first message for the e-mail attribute - * $email = $messages->first('email', '

:message

'); + * echo $messages->first('email', '

:message

'); *
* * @param string $key @@ -82,14 +80,14 @@ public function first($key, $format = ':message') } /** - * Get all of the messages for a key. + * Get all of the messages from the container for a given key. * * - * // Get all of the messages for the e-mail attribute - * $email = $messages->get('email'); + * // Echo all of the messages for the e-mail attribute + * echo $messages->get('email'); * * // Format all of the messages for the e-mail attribute - * $email = $messages->get('email', '

:message

'); + * echo $messages->get('email', '

:message

'); *
* * @param string $key @@ -107,7 +105,7 @@ public function get($key, $format = ':message') } /** - * Get all of the messages for every key. + * Get all of the messages for every key in the container. * * * // Get all of the messages in the collector diff --git a/laravel/paginator.php b/laravel/paginator.php index 033df32c..b08b8959 100644 --- a/laravel/paginator.php +++ b/laravel/paginator.php @@ -173,8 +173,8 @@ public function links($adjacent = 3) // // If there are not enough pages to make the creation of a slider possible // based on the adjacent pages, we will simply display all of the pages. - // Otherwise, we will create a "truncating" slider which displays a nice - // window of pages based on the current page. + // Otherwise, we will create a "truncating" slider which displays a + // nice window of pages based on the current page. if ($this->last < 7 + ($adjacent * 2)) { $links = $this->range(1, $this->last); @@ -288,7 +288,10 @@ protected function element($element, $page, $text, $disabled) { $class = "{$element}_page"; - if (is_null($text)) $text = Lang::line("pagination.{$element}")->get($this->language); + if (is_null($text)) + { + $text = Lang::line("pagination.{$element}")->get($this->language); + } // Each consumer of this method provides a "disabled" Closure which can // be used to determine if the element should be a span element or an @@ -383,12 +386,14 @@ protected function link($page, $text, $class) */ protected function appendage($appends) { - if ( ! is_null($this->appendage)) + if ( ! is_null($this->appendage)) return $this->appendage; + + if (count($appends) <= 0) { - return $this->appendage; + return $this->appendage = ''; } - return $this->appendage = (count($appends) > 0) ? '&'.http_build_query($appends) : ''; + return $this->appendage = '&'.http_build_query($appends); } /** diff --git a/laravel/redirect.php b/laravel/redirect.php index 828c40a6..2f19fa77 100644 --- a/laravel/redirect.php +++ b/laravel/redirect.php @@ -9,11 +9,8 @@ class Redirect extends Response { * // Create a redirect response to a location within the application * return Redirect::to('user/profile'); * - * // Create a redirect with a 301 status code + * // Create a redirect response with a 301 status code * return Redirect::to('user/profile', 301); - * - * // Create a redirect response to a location outside of the application - * return Redirect::to('http://google.com'); * * * @param string $url @@ -39,13 +36,48 @@ public static function to_secure($url, $status = 302) } /** - * Add an item to the session flash data. - * - * This is useful for passing status messages or other temporary data to the next request. + * Create a redirect response to a named route. * * - * // Create a redirect response and flash something to the session - * return Redirect::to('user/profile')->with('message', 'Welcome Back!'); + * // Create a redirect response to the "login" named route + * return Redirect::to_route('login'); + * + * // Create a redirect response to the "profile" named route with parameters + * return Redirect::to_route('profile', array($username)); + * + * + * @param string $route + * @param array $parameters + * @param int $status + * @param bool $https + * @return Redirect + */ + public static function to_route($route, $parameters = array(), $status = 302, $https = false) + { + return static::to(URL::to_route($route, $parameters, $https), $status); + } + + /** + * Create a redirect response to a named route using HTTPS. + * + * @param string $route + * @param array $parameters + * @param int $status + * @return Redirect + */ + public static function to_secure_route($route, $parameters = array(), $status = 302) + { + return static::to_route($route, $parameters, $status, true); + } + + /** + * Add an item to the session flash data. + * + * This is useful for "passing" status messages or other data to the next request. + * + * + * // Create a redirect response and flash to the session + * return Redirect::to('profile')->with('message', 'Welcome Back!'); * * * @param string $key @@ -56,10 +88,10 @@ public function with($key, $value) { if (Config::get('session.driver') == '') { - throw new \LogicException('A session driver must be set before setting flash data.'); + throw new \Exception('A session driver must be set before setting flash data.'); } - IoC::core('session')->flash($key, $value); + Session::flash($key, $value); return $this; } @@ -71,13 +103,13 @@ public function with($key, $value) * * * // Redirect and flash all of the input data to the session - * return Redirect::to_login()->with_input(); + * return Redirect::to('login')->with_input(); * * // Redirect and flash only a few of the input items - * return Redirect::to_login()->with_input('only', array('email', 'username')); + * return Redirect::to('login')->with_input('only', array('email', 'username')); * * // Redirect and flash all but a few of the input items - * return Redirect::to_login()->with_input('except', array('password', 'ssn')); + * return Redirect::to('login')->with_input('except', array('password', 'ssn')); * * * @param string $filter @@ -87,6 +119,7 @@ public function with($key, $value) public function with_input($filter = null, $items = array()) { Input::flash($filter, $items); + return $this; } @@ -96,11 +129,8 @@ public function with_input($filter = null, $items = array()) * This method allows you to conveniently pass validation errors back to views. * * - * // Redirect and flash a validator's errors the session + * // Redirect and flash validator errors the session * return Redirect::to('register')->with_errors($validator); - * - * // Redirect and flash a message container to the session - * return Redirect::to('register')->with_errors($messages); * * * @param Validator|Messages $container @@ -113,40 +143,4 @@ public function with_errors($container) return $this->with('errors', $errors); } - /** - * Magic Method to handle creation of redirects to named routes. - * - * - * // Create a redirect response to the "profile" named route - * return Redirect::to_profile(); - * - * // Create a redirect response to a named route using HTTPS - * return Redirect::to_secure_profile(); - * - * // Create a redirect response to a named route with wildcard parameters - * return Redirect::to_profile(array($username)); - * - */ - public static function __callStatic($method, $parameters) - { - // Extract the parameters that should be placed in the URL. These parameters - // are used to fill all of the wildcard slots in the route URI definition. - // They are passed as the first parameter to this magic method. - $wildcards = (isset($parameters[0])) ? $parameters[0] : array(); - - $status = (isset($parameters[1])) ? $parameters[1] : 302; - - if (strpos($method, 'to_secure_') === 0) - { - return static::to(URL::to_route(substr($method, 10), $wildcards, true), $status); - } - - if (strpos($method, 'to_') === 0) - { - return static::to(URL::to_route(substr($method, 3), $wildcards), $status); - } - - throw new \BadMethodCallException("Method [$method] is not defined on the Redirect class."); - } - } \ No newline at end of file diff --git a/laravel/redis.php b/laravel/redis.php index d23589a4..446b7f15 100644 --- a/laravel/redis.php +++ b/laravel/redis.php @@ -65,7 +65,7 @@ public static function db($name = 'default') { if (is_null($config = Config::get("database.redis.{$name}"))) { - throw new \DomainException("Redis database [$name] is not defined."); + throw new \Exception("Redis database [$name] is not defined."); } static::$databases[$name] = new static($config['host'], $config['port']); @@ -98,7 +98,7 @@ public function run($method, $parameters) switch (substr($response, 0, 1)) { case '-': - throw new \RuntimeException('Redis error: '.substr(trim($response), 4)); + throw new \Exception('Redis error: '.substr(trim($response), 4)); case '+': case ':': @@ -111,7 +111,7 @@ public function run($method, $parameters) return $this->multibulk($response); default: - throw new \UnexpectedValueException("Unknown Redis response: ".substr($response, 0, 1)); + throw new \Exception("Unknown Redis response: ".substr($response, 0, 1)); } } @@ -128,7 +128,7 @@ protected function connect() if ($this->connection === false) { - throw new \RuntimeException("Error making Redis connection: {$error} - {$message}"); + throw new \Exception("Error making Redis connection: {$error} - {$message}"); } return $this->connection; diff --git a/laravel/request.php b/laravel/request.php index 07f8657f..69d20a23 100644 --- a/laravel/request.php +++ b/laravel/request.php @@ -1,7 +1,6 @@ token(); + return Input::get(Session::csrf_token) !== Session::token(); } /** @@ -139,6 +128,16 @@ public static function ajax() return strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'; } + /** + * Determine if the current request is via the command line. + * + * @return bool + */ + public static function cli() + { + return defined('STDIN'); + } + /** * Get the route handling the current request. * diff --git a/laravel/response.php b/laravel/response.php index 7ee11e6c..a51c094c 100644 --- a/laravel/response.php +++ b/laravel/response.php @@ -14,7 +14,7 @@ class Response { * * @var int */ - public $status; + public $status = 200; /** * The response headers. @@ -28,7 +28,7 @@ class Response { * * @var array */ - protected $statuses = array( + public static $statuses = array( 100 => 'Continue', 101 => 'Switching Protocols', 200 => 'OK', @@ -103,7 +103,7 @@ public function __construct($content, $status = 200, $headers = array()) * return Response::make('Not Found', 404); * * // Create a response with some custom headers - * return Respone::make(json_encode($user), 200, array('content-type' => 'application/json')); + * return Response::make(json_encode($user), 200, array('header' => 'value')); *
* * @param mixed $content @@ -136,32 +136,12 @@ public static function view($view, $data = array()) return new static(View::make($view, $data)); } - /** - * Create a new response instance containing a named view. - * - * - * // Create a response with the "layout" named view - * return Response::of('layout'); - * - * // Create a response with the "layout" named view and data - * return Response::of('layout', array('name' => 'Taylor')); - * - * - * @param string $name - * @param array $data - * @return Response - */ - public static function of($name, $data = array()) - { - return new static(View::of($name, $data)); - } - /** * Create a new error response instance. * * The response status code will be set using the specified code. * - * Note: The specified error should match a view in your views/error directory. + * The specified error should match a view in your views/error directory. * * * // Create a 404 response @@ -215,21 +195,32 @@ public static function download($path, $name = null, $headers = array()) } /** - * Get the evaluated string contents of the response. + * Prepare a response from the given value. * - * @return string + * If the value is not a response, it will be converted into a response + * instance and the content will be cast to a string. + * + * @param mixed $response + * @return Response */ - public function render() + public static function prepare($response) { - return ($this->content instanceof View) ? $this->content->render() : (string) $this->content; + if ( ! $response instanceof Response) $response = new static($response); + + // We'll need to force the response to be a string before closing the session, + // since the developer may be using the session within a view, and we can't + // age the flash data until the view is rendered. + // + // Since this method is used by both the Route and Controller classes, it is + // a convenient spot to cast the application response to a string before it + // is returned to the main request handler. + $response->content = (string) $response->content; + + return $response; } /** - * Send the response to the browser. - * - * All of the response headers will be sent to the browser first, followed by - * the content of the response instance, which will be evaluated and rendered - * by the render method. + * Send the headers and content of the response to the browser. * * @return void */ @@ -237,47 +228,63 @@ public function send() { if ( ! headers_sent()) $this->send_headers(); - echo $this->render(); + echo (string) $this->content; } /** * Send all of the response headers to the browser. * - * The developer may set response headers using the "header" method. All of - * the headers set by the developer will be automatically sent to the browser - * when the response is sent via the "send" method. There is no need to call - * this method before calling the "send" method. - * - * The protocol and status header will be set automatically, as well as the - * content-type and charset, unless those headers have been set explicitly. - * The content-type charset used will be the application encoding. - * * @return void */ public function send_headers() { - if ( ! isset($this->headers['Content-Type'])) + // If the server is using FastCGI, we need to send a slightly different + // protocol and status header than we normally would. Otherwise it will + // not call any custom scripts setup to handle 404 responses. + // + // The status header will contain both the code and the status message, + // such as "OK" or "Not Found". For typical servers, the HTTP protocol + // will also be included with the status. + if (isset($_SERVER['FCGI_SERVER_VERSION'])) { - $encoding = Config::$items['application']['encoding']; - - $this->header('Content-Type', "text/html; charset={$encoding}"); + header('Status: '.$this->status.' '.$this->message()); + } + else + { + header(Request::protocol().' '.$this->status.' '.$this->message()); } - header(Request::protocol().' '.$this->status.' '.$this->statuses[$this->status]); + // If the content type was not set by the developer, we will set the + // header to a default value that indicates to the browser that the + // response is HTML and that it uses the default encoding. + if ( ! isset($this->headers['Content-Type'])) + { + $encoding = Config::get('application.encoding'); + $this->header('Content-Type', 'text/html; charset='.$encoding); + } + + // Once the framework controlled headers have been sentm, we can + // simply iterate over the developer's headers and send each one + // to the browser. Headers with the same name will be overriden. foreach ($this->headers as $name => $value) - { - header($name.': '.$value, true); + { + header("{$name}: {$value}", true); } } /** - * Add a header to the response. + * Get the status code message for the response. * - * - * // Add a header to a response instance - * return Response::make('foo')->header('content-type', 'application/json'); - * + * @return string + */ + public function message() + { + return static::$statuses[$this->status]; + } + + /** + * Add a header to the array of response headers. * * @param string $name * @param string $value @@ -301,24 +308,4 @@ public function status($status) return $this; } - /** - * Magic Method for handling the dynamic creation of Responses containing named views. - * - * - * // Create a response instance with the "layout" named view - * return Response::of_layout(); - * - * // Create a response instance with a named view and data - * return Response::of_layout(array('name' => 'Taylor')); - * - */ - public static function __callStatic($method, $parameters) - { - if (strpos($method, 'of_') === 0) - { - return static::of(substr($method, 3), Arr::get($parameters, 0, array())); - } - throw new \BadMethodCallException("Method [$method] is not defined on the Response class."); - } - } \ No newline at end of file diff --git a/laravel/routing/controller.php b/laravel/routing/controller.php index 66f17208..c8173996 100644 --- a/laravel/routing/controller.php +++ b/laravel/routing/controller.php @@ -1,7 +1,9 @@ + * // Call the "show" method on the "user" controller + * $response = Controller::call('user@show'); * - * For example, a destination of "user.profile@show" would call the User_Profile - * controller's show method with the given parameters. + * // Call the "profile" method on the "user/admin" controller and pass parameters + * $response = Controller::call('user.admin@profile', array($username)); + * * * @param string $destination * @param array $parameters @@ -44,43 +48,47 @@ abstract class Controller { */ public static function call($destination, $parameters = array()) { - if (strpos($destination, '@') === false) - { - throw new \InvalidArgumentException("Route delegate [{$destination}] has an invalid format."); - } + list($bundle, $destination) = Bundle::parse($destination); + + // We will always start the bundle, just in case the developer is pointing + // a route to another bundle. This allows us to lazy load the bundle and + // improve performance since the bundle is not loaded on every request. + Bundle::start($bundle); list($controller, $method) = explode('@', $destination); - $controller = static::resolve($controller); + $controller = static::resolve($bundle, $controller); - if (is_null($controller)) - { - return Response::error('404'); - } + // If the controller could not be resolved, we're out of options and will + // return the 404 error response. Of course, if we found the controller, + // we can go ahead and execute the requested method on the instance. + if (is_null($controller)) return Response::error('404'); return $controller->execute($method, $parameters); } /** - * Resolve a controller name to a controller instance. + * Resolve a bundle and controller name to a controller instance. * - * @param string $container + * @param string $bundle * @param string $controller * @return Controller */ - public static function resolve($controller) + public static function resolve($bundle, $controller) { - if ( ! static::load($controller)) return; + if ( ! static::load($bundle, $controller)) return; // If the controller is registered in the IoC container, we will resolve // it out of the container. Using constructor injection on controllers // via the container allows more flexible and testable applications. - if (IoC::registered('controllers.'.$controller)) + $resolver = 'controller: '.Bundle::identifier($bundle, $controller); + + if (IoC::registered($resolver)) { - return IoC::resolve('controllers.'.$controller); + return IoC::resolve($resolver); } - $controller = str_replace(' ', '_', ucwords(str_replace('.', ' ', $controller))).'_Controller'; + $controller = static::format($bundle, $controller); $controller = new $controller; @@ -89,7 +97,7 @@ public static function resolve($controller) // layout property, replacing the string layout name. if ( ! is_null($controller->layout)) { - $controller->layout = View::make($controller->layout); + $controller->layout = $controller->layout(); } return $controller; @@ -98,14 +106,15 @@ public static function resolve($controller) /** * Load the file for a given controller. * + * @param string $bundle * @param string $controller * @return bool */ - protected static function load($controller) + protected static function load($bundle, $controller) { $controller = strtolower(str_replace('.', '/', $controller)); - if (file_exists($path = CONTROLLER_PATH.$controller.EXT)) + if (file_exists($path = Bundle::path($bundle).'controllers/'.$controller.EXT)) { require_once $path; @@ -115,6 +124,24 @@ protected static function load($controller) return false; } + /** + * Format a bundle and controller identifier into the controller's class name. + * + * @param string $bundle + * @param string $controller + * @return string + */ + protected static function format($bundle, $controller) + { + // If the controller's bundle is not the application bundle, we will + // prepend the bundle to the identifier so the bundle is prefixed to + // the class name when it is formatted. Bundle controllers are + // always prefixed with the bundle's name by convention. + if ($bundle !== DEFAULT_BUNDLE) $controller = $bundle.'.'.$controller; + + return Str::classify($controller).'_Controller'; + } + /** * Execute a controller method with the given parameters. * @@ -124,47 +151,18 @@ protected static function load($controller) */ public function execute($method, $parameters = array()) { - // Again, as was the case with route closures, if the controller - // "before" filters return a response, it will be considered the - // response to the request and the controller method will not be - // used to handle the request to the application. + // Again, as was the case with route closures, if the controller "before" + // filters return a response, it will be considered the response to the + // request and the controller method will not be used to handle the + // request to the application. $response = Filter::run($this->filters('before', $method), array(), true); if (is_null($response)) { - // The developer may mark the controller as being "RESTful" which - // indicates that the controller actions are prefixed with the - // HTTP verb they respond to rather than the word "action". - if ($this->restful) - { - $action = strtolower(Request::method()).'_'.$method; - } - else - { - $action = "action_{$method}"; - } - - $response = call_user_func_array(array($this, $action), $parameters); - - // If the controller has specified a layout view. The response - // returned by the controller method will be bound to that view - // and the layout will be considered the response. - if (is_null($response) and ! is_null($this->layout)) - { - $response = $this->layout; - } + $response = $this->response($method, $parameters); } - if ( ! $response instanceof Response) - { - $response = new Response($response); - } - - // Stringify the response. We need to force the response to be - // stringed before closing the session, since the developer may - // be using the session within their views, so we cannot age - // the session data until the view is rendered. - $response->content = $response->render(); + $response = Response::prepare($response); Filter::run($this->filters('after', $method), array($response)); @@ -172,9 +170,45 @@ public function execute($method, $parameters = array()) } /** - * Register filters on the controller's methods. + * Execute a controller action and return the response. * - * Generally, this method will be used in the controller's constructor. + * Unlike the "execute" method, no filters will be run and the response + * from the controller action will not be changed in any way before it + * is returned to the consumer. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function response($method, $parameters = array()) + { + // The developer may mark the controller as being "RESTful" which + // indicates that the controller actions are prefixed with the + // HTTP verb they respond to rather than the word "action". + if ($this->restful) + { + $action = strtolower(Request::method()).'_'.$method; + } + else + { + $action = "action_{$method}"; + } + + $response = call_user_func_array(array($this, $action), $parameters); + + // If the controller has specified a layout view. The response + // returned by the controller method will be bound to that + // view and the layout will be considered the response. + if (is_null($response) and ! is_null($this->layout)) + { + $response = $this->layout; + } + + return $response; + } + + /** + * Register filters on the controller's methods. * * * // Set a "foo" after filter on the controller @@ -184,46 +218,54 @@ public function execute($method, $parameters = array()) * $this->filter('after', 'foo|bar')->only(array('user', 'profile')); * * + * @param string $event * @param string|array $filters + * @param mixed $parameters * @return Filter_Collection */ - protected function filter($name, $filters) + protected function filter($event, $filters, $parameters = null) { - $this->filters[$name][] = new Filter_Collection($name, $filters); + $this->filters[$event][] = new Filter_Collection($filters, $parameters); - return $this->filters[$name][count($this->filters[$name]) - 1]; + return $this->filters[$event][count($this->filters[$event]) - 1]; } /** * Get an array of filter names defined for the destination. * - * @param string $name + * @param string $event * @param string $method * @return array */ - protected function filters($name, $method) + protected function filters($event, $method) { - if ( ! isset($this->filters[$name])) return array(); + if ( ! isset($this->filters[$event])) return array(); $filters = array(); - foreach ($this->filters[$name] as $filter) + foreach ($this->filters[$event] as $collection) { - if ($filter->applies($method)) + if ($collection->applies($method)) { - $filters = array_merge($filters, $filter->filters); + $filters[] = $collection; } } - return array_unique($filters); + return $filters; + } + + /** + * Create the layout that is assigned to the controller. + * + * @return View + */ + public function layout() + { + return View::make($this->layout); } /** * Magic Method to handle calls to undefined functions on the controller. - * - * By default, the 404 response will be returned for an calls to undefined - * methods on the controller. However, this method may also be overridden - * and used as a pseudo-router by the controller. */ public function __call($method, $parameters) { @@ -243,12 +285,9 @@ public function __call($method, $parameters) */ public function __get($key) { - if (IoC::registered($key)) - { - return IoC::resolve($key); - } + if (IoC::registered($key)) return IoC::resolve($key); - throw new \OutOfBoundsException("Attempting to access undefined property [$key] on controller."); + throw new \Exception("Accessing undefined property [$key] on controller."); } } \ No newline at end of file diff --git a/laravel/routing/filter.php b/laravel/routing/filter.php index 48eb829a..539c4c57 100644 --- a/laravel/routing/filter.php +++ b/laravel/routing/filter.php @@ -1,5 +1,7 @@ + * // Register a closure as a filter + * Filter::register('before', function() {}); + * + * // Register a class callback as a filter + * Filter::register('before', array('Class', 'method')); + * + * + * @param string $name + * @param Closure $callback * @return void */ - public static function register($filters) + public static function register($name, Closure $callback) { - static::$filters = array_merge(static::$filters, $filters); + if (isset(static::$aliases[$name])) $name = static::$aliases[$name]; + + static::$filters[$name] = $callback; } /** - * Call a filter or set of filters. + * Alias a filter so it can be used by another name. * - * @param array|string $filters - * @param array $pass - * @param bool $override - * @return mixed + * This is convenient for shortening filters that are registered by bundles. + * + * @param string $filter + * @param string $alias + * @return void */ - public static function run($filters, $pass = array(), $override = false) + public static function alias($filter, $alias) { - foreach (static::parse($filters) as $filter) - { - $parameters = array(); - - // Parameters may be passed into routes by specifying the list of - // parameters after a colon. If parameters are present, we will - // merge them into the parameter array that was passed to the - // method and slice the parameters off of the filter string. - if (($colon = strpos($filter, ':')) !== false) - { - $parameters = explode(',', substr($filter, $colon + 1)); - - $filter = substr($filter, 0, $colon); - } - - if ( ! isset(static::$filters[$filter])) continue; - - $parameters = array_merge($pass, $parameters); - - $response = call_user_func_array(static::$filters[$filter], $parameters); - - // "Before" filters may override the request cycle. For example, - // an authentication filter may redirect a user to a login view - // if they are not logged in. Because of this, we will return - // the first filter response if overriding is enabled. - if ( ! is_null($response) and $override) return $response; - } + static::$aliases[$alias] = $filter; } /** - * Parse a string of filters into an array. + * Parse a filter definition into an array of filters. * * @param string|array $filters * @return array @@ -72,16 +67,67 @@ public static function parse($filters) return (is_string($filters)) ? explode('|', $filters) : (array) $filters; } + /** + * Call a filter or set of filters. + * + * @param array $collections + * @param array $pass + * @param bool $override + * @return mixed + */ + public static function run($collections, $pass = array(), $override = false) + { + foreach ($collections as $collection) + { + foreach ($collection->filters as $filter) + { + list($filter, $parameters) = $collection->get($filter); + + // We will also go ahead and start the bundle for the developer. This allows + // the developer to specify bundle filters on routes without starting the + // bundle manually, and performance is improved since the bundle is only + // started when needed. + Bundle::start(Bundle::name($filter)); + + if ( ! isset(static::$filters[$filter])) continue; + + $callback = static::$filters[$filter]; + + // Parameters may be passed into filters by specifying the list of parameters + // as an array, or by registering a Closure which will return the array of + // parameters. If parameters are present, we will merge them with the + // parameters that were given to the method. + $response = call_user_func_array($callback, array_merge($pass, $parameters)); + + // "Before" filters may override the request cycle. For example, an auth + // filter may redirect a user to a login view if they are not logged in. + // Because of this, we will return the first filter response if + // overriding is enabled for the filter collections + if ( ! is_null($response) and $override) + { + return $response; + } + } + } + } + } class Filter_Collection { /** - * The event being filtered. + * The filters contained by the collection. * - * @var string + * @var string|array */ - public $name; + public $filters = array(); + + /** + * The parameters specified for the filter. + * + * @var mixed + */ + public $parameters; /** * The included controller methods. @@ -97,13 +143,6 @@ class Filter_Collection { */ public $except = array(); - /** - * The filters contained by the collection. - * - * @var string|array - */ - public $filters = array(); - /** * The HTTP methods for which the filter applies. * @@ -114,21 +153,73 @@ class Filter_Collection { /** * Create a new filter collection instance. * - * @param string $name * @param string|array $filters + * @param mixed $parameters */ - public function __construct($name, $filters) + public function __construct($filters, $parameters = null) { - $this->name = $name; + $this->parameters = $parameters; $this->filters = Filter::parse($filters); } /** - * Determine if this collection's filters apply to a given method. + * Parse the filter string, returning the filter name and parameters. * - * Methods may be included / excluded using the "only" and "except" methods on the - * filter collection. Also, the "on" method may be used to set certain filters to - * only run when the request uses a given HTTP verb. + * @param string $filter + * @return array + */ + public function get($filter) + { + // If the parameters were specified by passing an array into the collection, + // then we will simply return those parameters. Combining passed parameters + // with parameters specified directly in the filter attachment is not + // currently supported by the framework. + if ( ! is_null($this->parameters)) + { + return array($filter, $this->parameters()); + } + + // If no parameters were specified when the collection was created, we will + // check the filter string itself to see if the parameters were injected + // into the string as raw values, such as "role:admin". + if (($colon = strpos(Bundle::element($filter), ':')) !== false) + { + $parameters = explode(',', substr(Bundle::element($filter), $colon + 1)); + + // If the filter belongs to a bundle, we need to re-calculate the position + // of the parameter colon, since we originally calculated it without the + // bundle identifier because the identifier uses colons as well. + if (($bundle = Bundle::name($filter)) !== DEFAULT_BUNDLE) + { + $colon = strlen($bundle.'::') + $colon; + } + + return array(substr($filter, 0, $colon), $parameters); + } + + // If no parameters were specified when the collection was created or + // in the filter string, we will just return the filter name as is + // and give back an empty array of parameters. + return array($filter, array()); + } + + /** + * Evaluate the collection's parameters and return a parameters array. + * + * @return array + */ + protected function parameters() + { + if ($this->parameters instanceof Closure) + { + $this->parameters = call_user_func($this->parameters); + } + + return $this->parameters; + } + + /** + * Determine if this collection's filters apply to a given method. * * @param string $method * @return bool @@ -145,7 +236,9 @@ public function applies($method) return false; } - if (count($this->methods) > 0 and ! in_array(strtolower(Request::method()), $this->methods)) + $request = strtolower(Request::method()); + + if (count($this->methods) > 0 and ! in_array($request, $this->methods)) { return false; } @@ -156,15 +249,12 @@ public function applies($method) /** * Set the excluded controller methods. * - * When methods are excluded, the collection's filters will be run for each - * controller method except those explicitly specified via this method. - * * * // Specify a filter for all methods except "index" * $this->filter('before', 'auth')->except('index'); * * // Specify a filter for all methods except "index" and "home" - * $this->filter('before', 'auth')->except('index', 'home'); + * $this->filter('before', 'auth')->except(array('index', 'home')); * * * @param array $methods @@ -172,23 +262,19 @@ public function applies($method) */ public function except($methods) { - $this->except = (count(func_get_args()) > 1) ? func_get_args() : (array) $methods; + $this->except = (array) $methods; return $this; } /** * Set the included controller methods. * - * This method is the inverse of the "except" methods. The methods specified - * via this method are the only controller methods on which the collection's - * filters will be run. - * * * // Specify a filter for only the "index" method * $this->filter('before', 'auth')->only('index'); * * // Specify a filter for only the "index" and "home" methods - * $this->filter('before', 'auth')->only('index', 'home'); + * $this->filter('before', 'auth')->only(array('index', 'home')); * * * @param array $methods @@ -196,23 +282,19 @@ public function except($methods) */ public function only($methods) { - $this->only = (count(func_get_args()) > 1) ? func_get_args() : (array) $methods; + $this->only = (array) $methods; return $this; } /** * Set the HTTP methods for which the filter applies. * - * Since some filters, such as the CSRF filter, only make sense in a POST - * request context, this method allows you to limit which HTTP methods - * the filter will apply to. - * * * // Specify that a filter only applies on POST requests * $this->filter('before', 'csrf')->on('post'); * * // Specify that a filter applies for multiple HTTP request methods - * $this->filter('before', 'csrf')->on('post', 'put'); + * $this->filter('before', 'csrf')->on(array('post', 'put')); * * * @param array $methods @@ -220,13 +302,7 @@ public function only($methods) */ public function on($methods) { - $methods = (count(func_get_args()) > 1) ? func_get_args() : (array) $methods; - - foreach ($methods as $method) - { - $this->methods[] = strtolower($method); - } - + $method = array_map('strtolower', (array) $methods); return $this; } diff --git a/laravel/routing/loader.php b/laravel/routing/loader.php deleted file mode 100644 index d4d6b80d..00000000 --- a/laravel/routing/loader.php +++ /dev/null @@ -1,120 +0,0 @@ -base = $base; - $this->nest = $nest; - } - - /** - * Load the applicable routes for a given URI. - * - * @param string $uri - * @return array - */ - public function load($uri) - { - $segments = Arr::without(explode('/', $uri), ''); - - return array_merge($this->nested($segments), require $this->base.'routes'.EXT); - } - - /** - * Get the appropriate routes from the routes directory for a given URI. - * - * This method works backwards through the URI segments until we find the - * deepest possible matching route directory. Once the deepest directory - * is found, all of the applicable routes will be returend. - * - * @param array $segments - * @return array - */ - protected function nested($segments) - { - foreach (array_reverse($segments, true) as $key => $value) - { - $path = $this->nest.implode('/', array_slice($segments, 0, $key + 1)).EXT; - - if (file_exists($path)) return require $path; - } - - return array(); - } - - /** - * Get every route defined for the application. - * - * The entire routes directory will be searched recursively to gather - * every route for the application. Of course, the routes in the root - * routes file will be returned as well. - * - * @return array - */ - public function everything() - { - if ( ! is_null($this->everything)) return $this->everything; - - $routes = array(); - - // First, we'll grab the base routes from the application directory. - // Once we have these, we'll merge all of the nested routes in the - // routes directory into this array of routes. - if (file_exists($path = $this->base.'routes'.EXT)) - { - $routes = array_merge($routes, require $path); - } - - if ( ! is_dir($this->nest)) return $routes; - - $iterator = new Iterator(new DirectoryIterator($this->nest), Iterator::SELF_FIRST); - - foreach ($iterator as $file) - { - // Since some Laravel developers may place HTML files in the route - // directories, we will check for the PHP extension before merging - // the file. Typically, the HTML files are present in installations - // that are not using mod_rewrite and the public directory. - if (filetype($file) === 'file' and strpos($file, EXT) !== false) - { - $routes = array_merge(require $file, $routes); - } - } - - return $this->everything = $routes; - } - -} \ No newline at end of file diff --git a/laravel/routing/route.php b/laravel/routing/route.php index 463c9eaa..ddabf660 100644 --- a/laravel/routing/route.php +++ b/laravel/routing/route.php @@ -1,7 +1,7 @@ key = $key; - $this->callback = $callback; + $this->action = $action; $this->parameters = $parameters; - // Extract each URI from the route key. Since the route key has the - // request method, we will extract that from the string. If the URI - // points to the root of the application, a single forward slash - // will be returned since that is used for the root route. - if (strpos($key, ', ') === false) - { - $this->uris = array($this->extract($this->key)); - } - else - { - $this->uris = array_map(array($this, 'extract'), explode(', ', $key)); - } + // Extract each URI from the route key. Since the route key has the request + // method, we will extract that from the string. If the URI points to the + // root of the application, a single forward slash will be returned. + $uris = array_get($action, 'handles', array()); - if ( ! $this->callable($callback)) - { - throw new \InvalidArgumentException('Invalid route defined for URI ['.$this->key.']'); - } - } + $this->uris = array_map(array($this, 'extract'), $uris); - /** - * Determine if the given route callback is callable. - * - * Route callbacks must be either a Closure, array, or string. - * - * @param mixed $callback - * @return bool - */ - protected function callable($callback) - { - return $callback instanceof Closure or is_array($callback) or is_string($callback); + // Determine the bundle in which the route was registered. We will know + // the bundle by the first segment of the route's URI. We need to know + // the bundle so we know if we need to run a bundle's global filters + // when executing the route. + $this->bundle = Bundle::resolve(head(explode('/', $this->uris[0]))); } /** * Retrieve the URI from a given route destination. * - * If the request is to the root of the application, a single slash - * will be returned, otherwise the leading slash will be removed. + * If the request is to the application root, a slash is returned. * * @param string $segment * @return string */ - protected function extract($segment) + protected static function extract($segment) { - $segment = substr($segment, strpos($segment, ' ') + 1); + $uri = substr($segment, strpos($segment, ' ') + 1); - return ($segment !== '/') ? trim($segment, '/') : $segment; + return ($uri !== '/') ? trim($uri, '/') : $uri; } /** @@ -103,116 +91,119 @@ protected function extract($segment) */ public function call() { - // Since "before" filters can halt the request cycle, we will return - // any response from the before filters. Allowing filters to halt the - // request cycle makes tasks like authorization convenient. - // // The route is responsible for running the global filters, and any // filters defined on the route itself. Since all incoming requests // come through a route (either defined or ad-hoc), it makes sense - // to let the route handle the global filters. If the route uses - // a controller, the controller will only call its own filters. - $before = array_merge(array('before'), $this->filters('before')); + // to let the route handle the global filters. + $response = Filter::run($this->filters('before'), array(), true); - $response = Filter::run($before, array(), true); - - if (is_null($response) and ! is_null($response = $this->response())) + if (is_null($response)) { - if ($response instanceof Delegate) - { - $response = Controller::call($response->destination, $this->parameters); - } + $response = $this->response(); } - if ( ! $response instanceof Response) - { - $response = new Response($response); - } + $response = Response::prepare($response); - // Stringify the response. We need to force the response to be - // stringed before closing the session, since the developer may - // be using the session within their views, so we cannot age - // the session data until the view is rendered. - $response->content = $response->render(); - - $filters = array_merge($this->filters('after'), array('after')); - - Filter::run($filters, array($response)); + Filter::run($this->filters('after'), array($response)); return $response; } /** - * Call the closure defined for the route, or get the route delegator. + * Execute the route action and return the response. * - * Note that this method differs from the "call" method in that it does - * not resolve the controller or prepare the response. Delegating to - * controller's is handled by the "call" method. + * Unlike the "call" method, none of the attached filters will be run. * * @return mixed */ - protected function response() + public function response() { - // If the route callback is an instance of a Closure, we can call the - // route function directly. There are no before or after filters to - // parse out of the route. - if ($this->callback instanceof Closure) + // If the action is a string, it is simply pointing the route to a + // controller action, and we can just call the action and return + // its response. This is the most basic form of route, and is + // the simplest to handle. + if ( ! is_null($delegate = $this->delegate())) { - return call_user_func_array($this->callback, $this->parameters); + return Controller::call($delegate, $this->parameters); } - // If the route is an array, we will return the first value with a - // key of "uses", or the first instance of a Closure. If the value - // is a string, the route is delegating the responsibility for - // for handling the request to a controller. - elseif (is_array($this->callback)) - { - $callback = Arr::first($this->callback, function($key, $value) - { - return $key == 'uses' or $value instanceof Closure; - }); - if ($callback instanceof Closure) - { - return call_user_func_array($callback, $this->parameters); - } - else - { - return new Delegate($callback); - } - } - elseif (is_string($this->callback)) + // If the route does not have a delegate, it should either be a + // Closure instance or have a Closure in its action array, so + // we will attempt to get the Closure and call it. + elseif ( ! is_null($handler = $this->handler())) { - return new Delegate($this->callback); + return call_user_func_array($handler, $this->parameters); } } /** - * Get an array of filter names defined for the route. + * Get the filters that are attached to the route for a given event. * - * @param string $name + * If the route belongs to a bundle, the bundle's global filters are returned too. + * + * @param string $filter * @return array */ - public function filters($name) + protected function filters($event) { - if (is_array($this->callback) and isset($this->callback[$name])) - { - $filters = $this->callback[$name]; + // Add the global filters to the array. We will also attempt to add + // the bundle's global filter as well. However, we'll need to keep + // the array unique since the default bundle's global filter will + // be the same as the application's global filter. + $filters = array_unique(array($event, Bundle::prefix($this->bundle).$event)); - return (is_string($filters)) ? explode('|', $filters) : (array) $filters; + // Next wee will check to see if there are any filters attached + // for the given event. If there are, we'll merge them in with + // the global filters for the application event. + if (isset($this->action[$event])) + { + $filters = array_merge($filters, Filter::parse($this->action[$event])); } - return array(); + return array(new Filter_Collection($filters)); + } + + /** + * Get the controller action delegate assigned to the route. + * + * If no delegate is assigned, null will be returned by the method. + * + * @return string + */ + protected function delegate() + { + return array_get($this->action, 'uses'); + } + + /** + * Get the anonymous function assigned to handle the route. + * + * If no anonymous function is assigned, null will be returned by the method. + * + * @return Closure + */ + protected function handler() + { + return array_first($this->action, function($key, $value) + { + return $value instanceof Closure; + }); } /** * Determine if the route has a given name. * + * + * // Determine if the route is the "login" route + * $login = Request::route()->is('login'); + * + * * @param string $name * @return bool */ public function is($name) { - return is_array($this->callback) and Arr::get($this->callback, 'name') === $name; + return is_array($this->action) and array_get($this->action, 'name') === $name; } /** @@ -223,20 +214,12 @@ public function is($name) */ public function handles($uri) { - return in_array($uri, $this->uris); - } + $pattern = '#'.str_replace('*', '(.*)', $uri).'#'; - /** - * Magic Method to handle dynamic method calls to determine the name of the route. - */ - public function __call($method, $parameters) - { - if (strpos($method, 'is_') === 0) + return ! is_null(array_first($this->uris, function($key, $uri) use ($pattern) { - return $this->is(substr($method, 3)); - } - - throw new \BadMethodCallException("Call to undefined method [$method] on Route class."); + return preg_match($pattern, $uri); + })); } } \ No newline at end of file diff --git a/laravel/routing/router.php b/laravel/routing/router.php index 91f6270c..1dcb595d 100644 --- a/laravel/routing/router.php +++ b/laravel/routing/router.php @@ -1,56 +1,27 @@ -destination = $destination; - } - -} + '([0-9]+)', '(:any)' => '([a-zA-Z0-9\.\-_]+)', ); @@ -60,44 +31,91 @@ class Router { * * @var array */ - protected $optional = array( + public static $optional = array( '/(:num?)' => '(?:/([0-9]+)', '/(:any?)' => '(?:/([a-zA-Z0-9\.\-_]+)', ); /** - * Create a new router for a request method and URI. + * Register a route with the router. * - * @param Loader $loader - * @param string $controllers + * + * // Register a route with the router + * Router::register('GET /', function() {return 'Home!';}); + * + * // Register a route that handles multiple URIs with the router + * Router::register(array('GET /', 'GET /home'), function() {return 'Home!';}); + * + * + * @param string|array $route + * @param string $action * @return void */ - public function __construct(Loader $loader, $controllers) + public static function register($route, $action) { - $this->loader = $loader; - $this->controllers = $controllers; + foreach ((array) $route as $uri) + { + // If the action is a string, it is a pointer to a controller, so we + // need to add it to the action array as a "uses" clause, which will + // indicate to the route to call the controller when the route is + // executed by the application. + // + // Note that all route actions are converted to arrays. This just + // gives us a convenient and consistent way of accessing it since + // we can always make an assumption that the action is an array, + // and it lets us store the URIs on the action for each route. + if (is_string($action)) + { + static::$routes[$uri]['uses'] = $action; + } + // If the action is not a string, we can just simply cast it as an + // array, then we will add all of the URIs to the action array as + // the "handes" clause so we can easily check which URIs are + // handled by the route instance. + else + { + // PHP 5.3.2 has a bug that causes closures cast as arrays + // to yield an empty array. We will work around this by + // manually adding the Closure instance to a new array. + if ($action instanceof Closure) $action = array($action); + + static::$routes[$uri] = (array) $action; + } + + static::$routes[$uri]['handles'] = (array) $route; + } } /** * Find a route by name. * - * The returned array will be identical the array defined in the routes.php file. * * @param string $name * @return array */ - public function find($name) + public static function find($name) { - if (array_key_exists($name, $this->names)) return $this->names[$name]; + if (isset(static::$names[$name])) return static::$names[$name]; - // To find a named route, we need to iterate through every route defined - // for the application. We will cache the routes by name so we can load - // them very quickly if we need to find them a second time. - foreach ($this->loader->everything() as $key => $value) + // If no route names have been found at all, we will assume no reverse + // routing has been done, and we will load the routes file for all of + // the bundle that are installed for the application. + if (count(static::$names) == 0) { - if (is_array($value) and isset($value['name']) and $value['name'] === $name) + foreach (Bundle::all() as $bundle) { - return $this->names[$name] = array($key => $value); + Bundle::routes($bundle); + } + } + + // To find a named route, we will iterate through every route defined + // for the application. We will cache the routes by name so we can + // load them very quickly if we need to find them a second time. + foreach (static::$routes as $key => $value) + { + if (isset($value['name']) and $value['name'] == $name) + { + return static::$names[$name] = array($key => $value); } } } @@ -109,59 +127,55 @@ public function find($name) * @param string $uri * @return Route */ - public function route($method, $uri) + public static function route($method, $uri) { - $routes = $this->loader->load($uri); - // All route URIs begin with the request method and have a leading - // slash before the URI. We'll put the request method and URI into + // slash before the URI. We'll put the request method and URI in // that format so we can easily check for literal matches. $destination = $method.' /'.trim($uri, '/'); - if (isset($routes[$destination])) + if (array_key_exists($destination, static::$routes)) { - return new Route($destination, $routes[$destination], array()); + return new Route($destination, static::$routes[$destination], array()); } - // If no literal route match was found, we will iterate through all - // of the routes and check each of them one at a time, translating - // any wildcards in the route into actual regular expressions. - foreach ($routes as $keys => $callback) + // If we can't find a literal match, we'll iterate through all of + // the registered routes attempting to find a matching route that + // uses wildcards or regular expressions. + if ( ! is_null($route = static::search($destination))) { - // Only check the routes that couldn't be matched literally... - if (strpos($keys, '(') !== false or strpos($keys, ',') !== false) - { - if ( ! is_null($route = $this->match($destination, $keys, $callback))) - { - return $route; - } - } + return $route; } - return $this->controller($method, $uri, $destination); + // If there are no literal matches and no routes that match the + // request, we'll use convention to search for a controller to + // handle the request. If no controller can be found, the 404 + // error response will be returned by the application. + $segments = array_diff(explode('/', trim($uri, '/')), array('')); + + return static::controller(DEFAULT_BUNDLE, $method, $destination, $segments); } /** - * Attempt to match a given route destination to a given route. - * - * The destination's methods and URIs will be compared against the route's. - * If there is a match, the Route instance will be returned, otherwise null - * will be returned by the method. + * Attempt to match a destination to one of the registered routes. * * @param string $destination - * @param array $keys - * @param mixed $callback - * @return mixed + * @return Route */ - protected function match($destination, $keys, $callback) + protected static function search($destination) { - foreach (explode(', ', $keys) as $key) + foreach (static::$routes as $route => $action) { - if (preg_match('#^'.$this->wildcards($key).'$#', $destination, $parameters)) + // Since routes that don't use wildcards or regular expressions + // should have been caught by the literal route check, we will + // only check routes that have a parentheses, indicating that + // there are wildcards or regular expressions. + if (strpos($route, '(') !== false) { - array_shift($parameters); - - return new Route($keys, $callback, $parameters); + if (preg_match('#^'.static::wildcards($route).'$#', $destination, $parameters)) + { + return new Route($route, $action, array_slice($parameters, 1)); + } } } } @@ -169,32 +183,47 @@ protected function match($destination, $keys, $callback) /** * Attempt to find a controller for the incoming request. * + * @param string $bundle * @param string $method - * @param string $uri * @param string $destination + * @param array $segments * @return Route */ - protected function controller($method, $uri, $destination) + protected static function controller($bundle, $method, $destination, $segments) { - // If the request is to the root of the application, an ad-hoc route - // will be generated to the home controller's "index" method, making - // it the default controller method. - if ($uri === '/') return new Route($method.' /', 'home@index'); - - $segments = explode('/', trim($uri, '/')); - - // If there are more than 20 request segments, we will halt the request - // and throw an exception. This is primarily to protect against DDoS - // attacks which could overwhelm the server by feeding it too many - // segments in the URI, causing the loops in this class to bog. - if (count($segments) > 20) + // If there are no more segments in the URI, we will just create a route + // for the default controller of the bundle, which is "home". We'll also + // use the default method, which is "index". + if (count($segments) == 0) { - throw new \Exception("Invalid request. There are more than 20 URI segments."); + $uri = ($bundle == DEFAULT_BUNDLE) ? '/' : "/{$bundle}"; + + $action = array('uses' => Bundle::prefix($bundle).'home@index'); + + return new Route($method.' '.$uri, $action); } - if ( ! is_null($key = $this->controller_key($segments))) + $directory = Bundle::path($bundle).'controllers/'; + + // We need to determine in which directory to look for the controllers. + // If the first segment of the request corresponds to a bundle that + // is installed for the application, we will use that bundle's + // controller path, otherwise we'll use the application's. + if (Bundle::routable($segments[0])) + { + $bundle = $segments[0]; + + // We shift the bundle name off of the URI segments because it will not + // be used to find a controller within the bundle. If we were to leave + // it in the segments, every bundle controller would need to be nested + // within a sub-directory matching the bundle name. + array_shift($segments); + + return static::controller($bundle, $method, $destination, $segments); + } + + if ( ! is_null($key = static::controller_key($segments, $directory))) { - // Extract the various parts of the controller call from the URI. // First, we'll extract the controller name, then, since we need // to extract the method and parameters, we will remove the name // of the controller from the URI. Then we can shift the method @@ -206,31 +235,37 @@ protected function controller($method, $uri, $destination) $method = (count($segments) > 0) ? array_shift($segments) : 'index'; - return new Route($destination, $controller.'@'.$method, $segments); + // We need to grab the prefix to the bundle so we can prefix + // the route identifier with it. This informs the controller + // class out of which bundle the controller instance should + // be resolved when it is needed by the application. + $prefix = Bundle::prefix($bundle); + + $action = array('uses' => $prefix.$controller.'@'.$method); + + return new Route($destination, $action, $segments); } } /** - * Search for a controller that can handle the current request. + * Get the URI index for the controller that should handle the request. * - * If a controller is found, the array key for the controller name in the URI - * segments will be returned by the method, otherwise NULL will be returned. - * The deepest possible controller will be considered the controller that - * should handle the request. - * - * @param array $segments + * @param string $directory + * @param array $segments * @return int */ - protected function controller_key($segments) + protected static function controller_key($segments, $directory) { // To find the proper controller, we need to iterate backwards through // the URI segments and take the first file that matches. That file - // should be the deepest controller matched by the URI. - foreach (array_reverse($segments, true) as $key => $value) + // should be the deepest possible controller matched by the URI. + $reverse = array_reverse($segments, true); + + foreach ($reverse as $key => $value) { $controller = implode('/', array_slice($segments, 0, $key + 1)).EXT; - if (file_exists($path = $this->controllers.$controller)) + if (file_exists($directory.$controller)) { return $key + 1; } @@ -243,16 +278,16 @@ protected function controller_key($segments) * @param string $key * @return string */ - protected function wildcards($key) + protected static function wildcards($key) { // For optional parameters, first translate the wildcards to their - // regex equivalent, sans the ")?" ending. We will add the endings + // regex equivalent, sans the ")?" ending. We'll add the endings // back on after we know how many replacements we made. - $key = str_replace(array_keys($this->optional), array_values($this->optional), $key, $count); + $key = str_replace(array_keys(static::$optional), array_values(static::$optional), $key, $count); $key .= ($count > 0) ? str_repeat(')?', $count) : ''; - return str_replace(array_keys($this->patterns), array_values($this->patterns), $key); + return str_replace(array_keys(static::$patterns), array_values(static::$patterns), $key); } } \ No newline at end of file diff --git a/laravel/section.php b/laravel/section.php index dc8991c3..d913a12e 100644 --- a/laravel/section.php +++ b/laravel/section.php @@ -19,31 +19,21 @@ class Section { /** * Start injecting content into a section. * - * After calling this method, the "stop" method may be used to stop injecting - * content. A raw string may also be passed as the second argument, and will - * cause the given string to be injected into the section directly without - * using output buffering. - * * * // Start injecting into the "header" section * Section::start('header'); * - * // Inject a raw string into the "header" section + * // Inject a raw string into the "header" section without buffering * Section::start('header', 'Laravel'); * * - * @param string $section - * @param string $content + * @param string $section + * @param string|Closure $content * @return void */ public static function start($section, $content = '') { - if ($content == '') - { - ob_start(); - - static::$last[] = $section; - } + if ($content === '') ob_start() and static::$last[] = $section; static::append($section, $content); } diff --git a/laravel/session.php b/laravel/session.php new file mode 100644 index 00000000..e775e483 --- /dev/null +++ b/laravel/session.php @@ -0,0 +1,117 @@ + + * // Retrieve the session instance and get an item + * Session::instance()->get('name'); + * + * // Retrieve the session instance and place an item in the session + * Session::instance()->put('name', 'Taylor'); + * + * + * @return Payload + */ + public static function instance() + { + if (static::started()) return static::$instance; + + throw new \Exception("A driver must be set before using the session."); + } + + /** + * Determine if session handling has been started for the request. + * + * @return bool + */ + public static function started() + { + return ! is_null(static::$instance); + } + + /** + * Magic Method for calling the methods on the session singleton instance. + * + * + * // Retrieve a value from the session + * $value = Session::get('name'); + * + * // Write a value to the session storage + * $value = Session::put('name', 'Taylor'); + * + * // Equivalent statement using the "instance" method + * $value = Session::instance()->put('name', 'Taylor'); + * + */ + public static function __callStatic($method, $parameters) + { + return call_user_func_array(array(static::$instance, $method), $parameters); + } + +} \ No newline at end of file diff --git a/laravel/session/drivers/database.php b/laravel/session/drivers/database.php index ec6cda02..6279b10e 100644 --- a/laravel/session/drivers/database.php +++ b/laravel/session/drivers/database.php @@ -101,7 +101,7 @@ public function sweep($expiration) */ private function table() { - return $this->connection->table(Config::$items['session']['table']); + return $this->connection->table(Config::get('session.table')); } } \ No newline at end of file diff --git a/laravel/session/drivers/factory.php b/laravel/session/drivers/factory.php deleted file mode 100644 index d8fdd625..00000000 --- a/laravel/session/drivers/factory.php +++ /dev/null @@ -1,40 +0,0 @@ -session) or $this->invalid()) + // so they can know whether to insert or update the session. + if (is_null($this->session) or static::expired($this->session)) { $this->exists = false; @@ -81,10 +74,10 @@ public function load($id) // A CSRF token is stored in every session. The token is used by the Form // class and the "csrf" filter to protect the application from cross-site // request forgery attacks. The token is simply a long, random string - // which should be posted with each request. - if ( ! $this->has(Payload::csrf_token)) + // which should be posted with each request to the application. + if ( ! $this->has(Session::csrf_token)) { - $this->put(Payload::csrf_token, Str::random(40)); + $this->put(Session::csrf_token, Str::random(40)); } } @@ -93,23 +86,14 @@ public function load($id) * * The session is considered valid if it exists and has not expired. * + * @param array $session * @return bool */ - protected function invalid() + protected static function expired($session) { - $lifetime = Config::$items['session']['lifetime']; + $lifetime = Config::get('session.lifetime'); - return (time() - $this->session['last_activity']) > ($lifetime * 60); - } - - /** - * Determine if session handling has been started for the request. - * - * @return bool - */ - public function started() - { - return is_array($this->session); + return (time() - $session['last_activity']) > ($lifetime * 60); } /** @@ -148,39 +132,48 @@ public function get($key, $default = null) // does not exist in that data, we will attempt to find it in the new // and old flash data. If none of those arrays contain the requested // item, we will just return the default value. - if ( ! is_null($value = Arr::get($session, $key))) + if ( ! is_null($value = array_get($session, $key))) { return $value; } - elseif ( ! is_null($value = Arr::get($session[':new:'], $key))) + elseif ( ! is_null($value = array_get($session[':new:'], $key))) { return $value; } - elseif ( ! is_null($value = Arr::get($session[':old:'], $key))) + elseif ( ! is_null($value = array_get($session[':old:'], $key))) { return $value; } - return ($default instanceof Closure) ? call_user_func($default) : $default; + return value($default); } /** * Write an item to the session. * + * + * // Write an item to the session payload + * Session::put('name', 'Taylor'); + * + * * @param string $key * @param mixed $value * @return void */ public function put($key, $value) { - Arr::set($this->session['data'], $key, $value); + array_set($this->session['data'], $key, $value); } /** * Write an item to the session flash data. * - * Flash data only exists for the next request to the application, and is - * useful for storing temporary data such as error or status messages. + * Flash data only exists for the current and next request to the application. + * + * + * // Write an item to the session payload's flash data + * Session::flash('name', 'Taylor'); + * * * @param string $key * @param mixed $value @@ -188,11 +181,11 @@ public function put($key, $value) */ public function flash($key, $value) { - Arr::set($this->session['data'][':new:'], $key, $value); + array_set($this->session['data'][':new:'], $key, $value); } /** - * Keep the session flash data from expiring at the end of the request. + * Keep all of the session flash data from expiring after the request. * * @return void */ @@ -206,6 +199,14 @@ public function reflash() /** * Keep a session flash item from expiring at the end of the request. * + * + * // Keep the "name" item from expiring from the flash data + * Session::keep('name'); + * + * // Keep the "name" and "email" items from expiring from the flash data + * Session::keep(array('name', 'email')); + * + * * @param string|array $key * @return void */ @@ -225,20 +226,23 @@ public function keep($keys) */ public function forget($key) { - Arr::forget($this->session['data'], $key); + array_forget($this->session['data'], $key); } /** * Remove all of the items from the session. * + * The CSRF token will not be removed from the session. + * * @return void */ public function flush() { - $this->session['data'] = array( - ':new:' => array(), - ':old:' => array(), - ); + $token = $this->token(); + + $session = array(Session::csrf_token => $token, ':new:' => array(), ':old:' => array()); + + $this->session['data'] = $session; } /** @@ -260,19 +264,13 @@ public function regenerate() */ public function token() { - return $this->get(Payload::csrf_token); + return $this->get(Session::csrf_token); } /** * Store the session payload in storage. * - * The activity timestamp will be set, the flash data will be aged, and the - * session cookie will be written. The driver given when the session payload - * was constructed will be used to persist the session to storage. - * - * If the session's driver is a sweeper implementation, garbage collection - * may be performed based on the probabilities set in the "sweepage" option - * in the session configuration file. + * This method will be called automatically at the end of the request. * * @return void */ @@ -280,19 +278,31 @@ public function save() { $this->session['last_activity'] = time(); + // Session flash data is only available during the request in which it + // was flashed and the following request. We will age the data so that + // it expires at the end of the user's next request. $this->age(); - $config = Config::$items['session']; + $config = Config::get('session'); + // The responsibility of actually storing the session information in + // persistent storage is delegated to the driver instance being used + // by the session payload. + // + // This allows us to keep the payload very generic, while moving the + // platform or storage mechanism code into the specialized drivers, + // keeping our code very dry and organized. $this->driver->save($this->session, $config, $this->exists); - $this->cookie(); + // Next we'll write out the session cookie. This cookie contains the + // ID of the session, and will be used to determine the owner of the + // session on the user's subsequent requests to the application. + $this->cookie($config); - // Some session drivers implement the Sweeper interface, meaning that they - // must clean up expired sessions manually. If the driver is a sweeper, we - // need to determine if garbage collection should be run for the request. - // Since garbage collection can be expensive, the probability of it - // occuring is controlled by the "sweepage" configuration option. + // Some session drivers implement the Sweeper interface, meaning that + // they must clean up expired sessions manually. If the driver is a + // sweeper, we need to determine if garbage collection should be + // run for the request. $sweepage = $config['sweepage']; if ($this->driver instanceof Sweeper and (mt_rand(1, $sweepage[1]) <= $sweepage[0])) @@ -304,10 +314,6 @@ public function save() /** * Age the session flash data. * - * Session flash data is only available during the request in which it - * was flashed, and the request after that. To "age" the data, we will - * remove all of the :old: items and re-address the new items. - * * @return void */ protected function age() @@ -320,12 +326,11 @@ protected function age() /** * Send the session ID cookie to the browser. * + * @param array $config * @return void */ - protected function cookie() + protected function cookie($config) { - $config = Config::$items['session']; - extract($config, EXTR_SKIP); $minutes = ( ! $expire_on_close) ? $lifetime : 0; @@ -333,4 +338,4 @@ protected function cookie() Cookie::put($cookie, $this->session['id'], $minutes, $path, $domain, $secure); } -} +} \ No newline at end of file diff --git a/laravel/str.php b/laravel/str.php index c7efbdc6..5f58f55a 100644 --- a/laravel/str.php +++ b/laravel/str.php @@ -3,66 +3,15 @@ class Str { /** - * Convert a string to lowercase. + * Get the default string encoding for the application. * - * - * // Convert a string to lowercase - * echo Str::lower('STOP YELLING'); - * + * This method is simply a short-cut to Config::get('application.encoding'). * - * @param string $value * @return string */ - public static function lower($value) + public static function encoding() { - if (function_exists('mb_strtolower')) - { - return mb_strtolower($value, Config::$items['application']['encoding']); - } - - return strtolower($value); - } - - /** - * Convert a string to uppercase. - * - * - * // Convert a string to uppercase - * echo Str::upper('speak louder'); - * - * - * @param string $value - * @return string - */ - public static function upper($value) - { - if (function_exists('mb_strtoupper')) - { - return mb_strtoupper($value, Config::$items['application']['encoding']); - } - - return strtoupper($value); - } - - /** - * Convert a string to title case (ucwords equivalent). - * - * - * // Convert a string to title case - * echo Str::title('taylor otwell'); - * - * - * @param string $value - * @return string - */ - public static function title($value) - { - if (function_exists('mb_convert_case')) - { - return mb_convert_case($value, MB_CASE_TITLE, Config::$items['application']['encoding']); - } - - return ucwords(strtolower($value)); + return Config::get('application.encoding'); } /** @@ -70,7 +19,10 @@ public static function title($value) * * * // Get the length of a string - * echo Str::length('taylor otwell'); + * $length = Str::length('Taylor Otwell'); + * + * // Get the length of a multi-byte string + * $length = Str::length('Τάχιστη') * * * @param string $value @@ -78,12 +30,69 @@ public static function title($value) */ public static function length($value) { - if (function_exists('mb_strlen')) + return (MB_STRING) ? mb_strlen($value, static::encoding()) : strlen($value); + } + + /** + * Convert a string to lowercase. + * + * + * // Convert a string to lowercase + * $lower = Str::lower('Taylor Otwell'); + * + * // Convert a multi-byte string to lowercase + * $lower = Str::lower('Τάχιστη'); + * + * + * @param string $value + * @return string + */ + public static function lower($value) + { + return (MB_STRING) ? mb_strtolower($value, static::encoding()) : strtolower($value); + } + + /** + * Convert a string to uppercase. + * + * + * // Convert a string to uppercase + * $upper = Str::upper('Taylor Otwell'); + * + * // Convert a multi-byte string to uppercase + * $upper = Str::upper('Τάχιστη'); + * + * + * @param string $value + * @return string + */ + public static function upper($value) + { + return (MB_STRING) ? mb_strtoupper($value, static::encoding()) : strtoupper($value); + } + + /** + * Convert a string to title case (ucwords equivalent). + * + * + * // Convert a string to title case + * $title = Str::title('taylor otwell'); + * + * // Convert a multi-byte string to title case + * $title = Str::title('νωθρού κυνός'); + * + * + * @param string $value + * @return string + */ + public static function title($value) + { + if (MB_STRING) { - return mb_strlen($value, Config::$items['application']['encoding']); + return mb_convert_case($value, MB_CASE_TITLE, static::encoding()); } - return strlen($value); + return ucwords(strtolower($value)); } /** @@ -106,16 +115,52 @@ public static function limit($value, $limit = 100, $end = '...') { if (static::length($value) <= $limit) return $value; - if (function_exists('mb_substr')) + if (MB_STRING) { - return mb_substr($value, 0, $limit, Config::$items['application']['encoding']).$end; + return mb_substr($value, 0, $limit, static::encoding()).$end; } return substr($value, 0, $limit).$end; } /** - * Limit the number of words in a string + * Get the singular form of the given word. + * + * The word should be defined in the "strings" configuration file. + * + * @param string $value + * @return string + */ + public static function singular($value) + { + return array_get(array_flip(Config::get('strings.inflection')), strtolower($value), $value); + } + + /** + * Get the plural form of the given word. + * + * The word should be defined in the "strings" configuration file. + * + * + * // Returns the plural form of "child" + * $plural = Str::plural('child', 10); + * + * // Returns the singular form of "octocat" since count is one + * $plural = Str::plural('octocat', 1); + * + * + * @param string $value + * @return string + */ + public static function plural($value, $count = 2) + { + if ((int) $count == 1) return $value; + + return array_get(Config::get('strings.inflection'), strtolower($value), $value); + } + + /** + * Limit the number of words in a string. * * * // Returns "This is a..." @@ -132,41 +177,90 @@ public static function limit($value, $limit = 100, $end = '...') */ public static function words($value, $words = 100, $end = '...') { - $count = str_word_count($value, 1); + preg_match('/^\s*+(?:\S++\s*+){1,'.$words.'}/', $value, $matches); - if ($count <= $words) return $value; + if (static::length($value) == static::length($matches[0])) + { + $end = ''; + } - return implode(' ', array_slice($count, 0, $words)).$end; + return rtrim($matches[0]).$end; + } + + /** + * Generate a URL friendly "slug" from a given string. + * + * + * // Returns "this-is-my-blog-post" + * $slug = Str::slug('This is my blog post!'); + * + * // Returns "this_is_my_blog_post" + * $slug = Str::slug('This is my blog post!', '_'); + * + * + * @param string $title + * @param string $separator + * @return string + */ + public static function slug($title, $separator = '-') + { + $title = static::ascii($title); + + // Remove all characters that are not the separator, letters, numbers, or whitespace. + $title = preg_replace('![^'.preg_quote($separator).'\pL\pN\s]+!u', '', static::lower($title)); + + // Replace all separator characters and whitespace by a single separator + $title = preg_replace('!['.preg_quote($separator).'\s]+!u', $separator, $title); + + return trim($title, $separator); } /** * Convert a string to 7-bit ASCII. * - * - * // Returns "Deuxieme Article" - * echo Str::ascii('Deuxième Article'); - * + * This is helpful for converting UTF-8 strings for usage in URLs, etc. * * @param string $value * @return string */ public static function ascii($value) { - $foreign = Config::get('ascii'); + $foreign = Config::get('strings.ascii'); $value = preg_replace(array_keys($foreign), array_values($foreign), $value); return preg_replace('/[^\x09\x0A\x0D\x20-\x7E]/', '', $value); } + /** + * Convert a string to an underscored, camel-cased class name. + * + * This method is primarily used to format task and controller names. + * + * + * // Returns "Task_Name" + * $class = Str::classify('task_name'); + * + * // Returns "Taylor_Otwell" + * $class = Str::classify('taylor otwell') + * + * + * @param string $value + * @return string + */ + public static function classify($value) + { + return str_replace(' ', '_', static::title(str_replace(array('_', '.'), ' ', $value))); + } + /** * Generate a random alpha or alpha-numeric string. * * - * // Generate a 40 character random, alpha-numeric string + * // Generate a 40 character random alpha-numeric string * echo Str::random(40); * - * // Generate a 16 character random, alphabetic string + * // Generate a 16 character random alphabetic string * echo Str::random(16, 'alpha'); * * @@ -196,8 +290,8 @@ protected static function pool($type) return '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; default: - throw new \DomainException("Invalid random string type [$type]."); + throw new \Exception("Invalid random string type [$type]."); } } -} +} \ No newline at end of file diff --git a/laravel/uri.php b/laravel/uri.php index ca7b611b..e94b16a4 100644 --- a/laravel/uri.php +++ b/laravel/uri.php @@ -14,15 +14,11 @@ class URI { * * @var array */ - protected static $segments = array(); + public static $segments = array(); /** * Get the URI for the current request. * - * If the request is to the root of the application, a single forward slash - * will be returned. Otherwise, the URI will be returned with all of the - * leading and trailing slashes removed. - * * @return string */ public static function current() @@ -36,14 +32,23 @@ public static function current() // rid of all of the the sub-directories from the request URI. $uri = static::remove($uri, parse_url(URL::base(), PHP_URL_PATH)); - if (($index = '/'.Config::$items['application']['index']) !== '/') + // We'll also remove the application's index page as it is not used for at + // all for routing and is totally unnecessary as far as the framework is + // concerned. It is only in the URI when mod_rewrite is not available. + if (($index = '/'.Config::get('application.index')) !== '/') { $uri = static::remove($uri, $index); } static::$uri = static::format($uri); - static::$segments = explode('/', static::$uri); + // Cache the URI segments. This allows us to avoid having to explode + // the segments every time the developer requests one of them. The + // extra slashes have already been stripped off of the URI so no + // extraneous elements should be present in the segment array. + $segments = explode('/', trim(static::$uri, '/')); + + static::$segments = array_diff($segments, array('')); return static::$uri; } @@ -67,7 +72,7 @@ public static function segment($index, $default = null) { static::current(); - return Arr::get(static::$segments, $index - 1, $default); + return array_get(static::$segments, $index - 1, $default); } /** @@ -79,19 +84,12 @@ public static function segment($index, $default = null) */ protected static function remove($uri, $value) { - if (strpos($uri, $value) === 0) - { - return substr($uri, strlen($value)); - } - return $uri; + return (strpos($uri, $value) === 0) ? substr($uri, strlen($value)) : $uri; } /** * Format a given URI. * - * If the URI is an empty string, a single forward slash will be returned. - * Otherwise, we will trim the URI's leading and trailing slashes. - * * @param string $uri * @return string */ diff --git a/laravel/url.php b/laravel/url.php index 78fc9cee..30f83216 100644 --- a/laravel/url.php +++ b/laravel/url.php @@ -5,24 +5,20 @@ class URL { /** * Get the base URL of the application. * - * If the application URL is explicitly defined in the application configuration - * file, that URL will be returned. Otherwise, the URL will be guessed based on - * the host and script name available in the global $_SERVER array. - * * @return string */ public static function base() { - if (($base = Config::$items['application']['url']) !== '') return $base; + if (($base = Config::get('application.url')) !== '') return $base; if (isset($_SERVER['HTTP_HOST'])) { $protocol = (Request::secure()) ? 'https://' : 'http://'; - // By removing the basename of the script, we should be left with the path - // in which the framework is installed. For example, if the framework is - // installed to http://localhost/laravel/public, the path we'll get from - // this statement will be "/laravel/public". + // Basically, by removing the basename, we are removing everything after the + // and including the front controller from the request URI. Leaving us with + // the path in which the framework is installed. From that path, we can + // construct the base URL to the application. $path = str_replace(basename($_SERVER['SCRIPT_NAME']), '', $_SERVER['SCRIPT_NAME']); return rtrim($protocol.$_SERVER['HTTP_HOST'].$path, '/'); @@ -50,12 +46,12 @@ public static function to($url = '', $https = false) { if (filter_var($url, FILTER_VALIDATE_URL) !== false) return $url; - $root = static::base().'/'.Config::$items['application']['index']; + $root = static::base().'/'.Config::get('application.index'); - // Since SSL is often not used while developing the application, we allow the + // Since SSL is not often used while developing the application, we allow the // developer to disable SSL on all framework generated links to make it more - // convenient to work with the site while developing. - if ($https and Config::$items['application']['ssl']) + // convenient to work with the site while developing locally. + if ($https and Config::get('application.ssl')) { $root = preg_replace('~http://~', 'https://', $root, 1); } @@ -74,86 +70,6 @@ public static function to_secure($url = '') return static::to($url, true); } - /** - * Generate an application URL to an asset. - * - * @param string $url - * @param bool $https - * @return string - */ - public static function to_asset($url, $https = null) - { - if (is_null($https)) $https = Request::secure(); - - $url = static::to($url, $https); - - // Since assets are not served by Laravel, we do not need to come through - // the front controller. We'll remove the application index specified in - // the application configuration from the generated URL. - if (($index = Config::$items['application']['index']) !== '') - { - $url = str_replace($index.'/', '', $url); - } - - return $url; - } - - /** - * Generate a URL from a route name. - * - * For routes that have wildcard parameters, an array may be passed as the - * second parameter to the method. The values of this array will be used to - * fill the wildcard segments of the route URI. - * - * - * // Create a URL to the "profile" named route - * $url = URL::to_route('profile'); - * - * // Create a URL to the "profile" named route with wildcard parameters - * $url = URL::to_route('profile', array($username)); - * - * - * @param string $name - * @param array $parameters - * @param bool $https - * @return string - */ - public static function to_route($name, $parameters = array(), $https = false) - { - if ( ! is_null($route = IoC::core('routing.router')->find($name))) - { - $uris = explode(', ', key($route)); - - $uri = substr($uris[0], strpos($uris[0], '/')); - - // Spin through each route parameter and replace the route wildcard - // segment with the corresponding parameter passed to the method. - // Afterwards, we will replace all of the remaining optional URI - // segments with spaces since they may not have been specified - // in the array of parameters. - foreach ((array) $parameters as $parameter) - { - $uri = preg_replace('/\(.+?\)/', $parameter, $uri, 1); - } - - return static::to(str_replace(array('/(:any?)', '/(:num?)'), '', $uri), $https); - } - - throw new \OutOfBoundsException("Error creating URL for undefined route [$name]."); - } - - /** - * Generate a HTTPS URL from a route name. - * - * @param string $name - * @param array $parameters - * @return string - */ - public static function to_secure_route($name, $parameters = array()) - { - return static::to_route($name, $parameters, true); - } - /** * Generate a URL to a controller action. * @@ -180,14 +96,8 @@ public static function to_action($action, $parameters = array(), $https = false) /** * Generate a HTTPS URL to a controller action. * - * - * // Generate a HTTPS URL to the "index" method of the "user" controller - * $url = URL::to_action('user@index'); - * - * * @param string $action * @param array $parameters - * @param bool $https * @return string */ public static function to_secure_action($action, $parameters = array()) @@ -196,62 +106,86 @@ public static function to_secure_action($action, $parameters = array()) } /** - * Generate a URL friendly "slug". + * Generate an application URL to an asset. * - * - * // Returns "this-is-my-blog-post" - * $slug = URL::slug('This is my blog post!'); - * - * // Returns "this_is_my_blog_post" - * $slug = URL::slug('This is my blog post!', '_'); - * - * - * @param string $title - * @param string $separator + * @param string $url + * @param bool $https * @return string */ - public static function slug($title, $separator = '-') + public static function to_asset($url, $https = null) { - $title = Str::ascii($title); + if (is_null($https)) $https = Request::secure(); - // Remove all characters that are not the separator, letters, numbers, or whitespace. - $title = preg_replace('![^'.preg_quote($separator).'\pL\pN\s]+!u', '', Str::lower($title)); + $url = static::to($url, $https); - // Replace all separator characters and whitespace by a single separator - $title = preg_replace('!['.preg_quote($separator).'\s]+!u', $separator, $title); + // Since assets are not served by Laravel, we do not need to come through + // the front controller. So, we'll remove the application index specified + // in the application configuration from the generated URL. + if (($index = Config::get('application.index')) !== '') + { + $url = str_replace($index.'/', '', $url); + } - return trim($title, $separator); + return $url; } /** - * Magic Method for dynamically creating URLs to named routes. + * Generate a URL from a route name. * * * // Create a URL to the "profile" named route - * $url = URL::to_profile(); + * $url = URL::to_route('profile'); * - * // Create a URL to the "profile" named route with wildcard segments - * $url = URL::to_profile(array($username)); - * - * // Create a URL to the "profile" named route using HTTPS - * $url = URL::to_secure_profile(); + * // Create a URL to the "profile" named route with wildcard parameters + * $url = URL::to_route('profile', array($username)); * + * + * @param string $name + * @param array $parameters + * @param bool $https + * @return string */ - public static function __callStatic($method, $parameters) + public static function to_route($name, $parameters = array(), $https = false) { - $parameters = (isset($parameters[0])) ? $parameters[0] : array(); - - if (strpos($method, 'to_secure_') === 0) + if (is_null($route = Routing\Router::find($name))) { - return static::to_route(substr($method, 10), $parameters, true); + throw new \Exception("Error creating URL for undefined route [$name]."); } - if (strpos($method, 'to_') === 0) + $uris = explode(', ', key($route)); + + // Routes can handle more than one URI, but we will just take the first URI + // and use it for the URL. Since all of the URLs should point to the same + // route, it doesn't make a difference. + $uri = substr($uris[0], strpos($uris[0], '/')); + + // Spin through each route parameter and replace the route wildcard segment + // with the corresponding parameter passed to the method. Afterwards, we'll + // replace all of the remaining optional URI segments with spaces since + // they may not have been specified in the array of parameters. + foreach ((array) $parameters as $parameter) { - return static::to_route(substr($method, 3), $parameters); + $uri = preg_replace('/\(.+?\)/', $parameter, $uri, 1); } - throw new \BadMethodCallException("Method [$method] is not defined on the URL class."); + // If there are any remaining optional place-holders, we'll just replace + // them with empty strings since not every optional parameter has to be + // in the array of parameters that were passed into the method. + $uri = str_replace(array('/(:any?)', '/(:num?)'), '', $uri); + + return static::to($uri, $https); + } + + /** + * Generate a HTTPS URL from a route name. + * + * @param string $name + * @param array $parameters + * @return string + */ + public static function to_secure_route($name, $parameters = array()) + { + return static::to_route($name, $parameters, true); } } \ No newline at end of file diff --git a/laravel/validator.php b/laravel/validator.php index 9ce05637..f7912065 100644 --- a/laravel/validator.php +++ b/laravel/validator.php @@ -1,7 +1,4 @@ -parse($rule); - $value = Arr::get($this->attributes, $attribute); + $value = array_get($this->attributes, $attribute); + // Before running the validator, we need to verify that the attribute and rule + // combination is actually validatable. Only the "accepted" rule implies that + // the attribute is "required", so if the attribute does not exist, the other + // rules will not be run for the attribute. $validatable = $this->validatable($rule, $attribute, $value); if ($validatable and ! $this->{'validate_'.$rule}($attribute, $value, $parameters, $this)) @@ -171,9 +179,9 @@ protected function check($attribute, $rule) /** * Determine if an attribute is validatable. * - * To be considered validatable, the attribute must either exist, or the - * rule being checked must implicitly validate "required", such as the - * "required" rule or the "accepted" rule. + * To be considered validatable, the attribute must either exist, or the rule + * being checked must implicitly validate "required", such as the "required" + * rule or the "accepted" rule. * * @param string $rule * @param string $attribute @@ -206,9 +214,7 @@ protected function implicit($rule) */ protected function error($attribute, $rule, $parameters) { - $message = $this->message($attribute, $rule); - - $message = $this->replace($message, $attribute, $rule, $parameters); + $message = $this->replace($this->message($attribute, $rule), $attribute, $rule, $parameters); $this->errors->add($attribute, $message); } @@ -347,17 +353,16 @@ protected function validate_max($attribute, $value, $parameters) /** * Get the size of an attribute. * - * This method will determine if the attribute is a number, string, or file and - * return the proper size accordingly. If it is a number, then number itself is - * the size; if it is a file, the size is kilobytes in the size; if it is a - * string, the length is the size. - * * @param string $attribute * @param mixed $value * @return mixed */ protected function size($attribute, $value) { + // This method will determine if the attribute is a number, string, or file and + // return the proper size accordingly. If it is a number, then number itself is + // the size; if it is a file, the size is kilobytes in the size; if it is a + // string, the length is the size. if (is_numeric($value) and $this->has_rule($attribute, $this->numeric_rules)) { return $this->attributes[$attribute]; @@ -410,11 +415,16 @@ protected function validate_not_in($attribute, $value, $parameters) */ protected function validate_unique($attribute, $value, $parameters) { - if ( ! isset($parameters[1])) $parameters[1] = $attribute; + if (is_null($this->db)) $this->db = Database::connection(); - if (is_null($this->db)) $this->db = DB::connection(); + $query = $this->db->table($parameters[0])->where($attribute, '=', $value); - return $this->db->table($parameters[0])->where($parameters[1], '=', $value)->count() == 0; + if (isset($parameters[1])) + { + $query->where('id', '<>', $parameters[1]); + } + + return $query->count() == 0; } /** @@ -513,7 +523,7 @@ protected function validate_alpha_dash($attribute, $value) */ protected function validate_mimes($attribute, $value, $parameters) { - if ( ! is_array($value) or Arr::get($value, 'tmp_name', '') == '') return true; + if ( ! is_array($value) or array_get($value, 'tmp_name', '') == '') return true; foreach ($parameters as $extension) { @@ -535,6 +545,8 @@ protected function validate_mimes($attribute, $value, $parameters) */ protected function message($attribute, $rule) { + $bundle = Bundle::prefix($this->bundle); + // First we'll check for developer specified, attribute specific messages. // These messages take first priority. They allow the fine-grained tuning // of error messages for each rule. @@ -556,16 +568,7 @@ protected function message($attribute, $rule) // either a number, file, or string. elseif (in_array($rule, $this->size_rules)) { - if ($this->has_rule($attribute, $this->numeric_rules)) - { - $line = 'numeric'; - } - else - { - $line = (array_key_exists($attribute, Input::file())) ? 'file' : 'string'; - } - - return Lang::line("validation.{$rule}.{$line}")->get($this->language); + return $this->size_message($bundle, $attribute, $rule); } // If no developer specified messages have been set, and no other special @@ -573,10 +576,43 @@ protected function message($attribute, $rule) // message from the validation language file. else { - return Lang::line("validation.{$rule}")->get($this->language); + $line = "{$bundle}validation.{$rule}"; + + return Lang::line($line)->get($this->language); } } + /** + * Get the proper error message for an attribute and size rule. + * + * @param string $bundle + * @param string $attribute + * @param string $rule + * @return string + */ + protected function size_message($bundle, $attribute, $rule) + { + // There are three different types of size validations. The attribute + // may be either a number, file, or a string. If the attribute has a + // numeric rule attached to it, we can assume it is a number. If the + // attribute is in the file array, it is a file, otherwise we can + // assume the attribute is simply a string. + if ($this->has_rule($attribute, $this->numeric_rules)) + { + $line = 'numeric'; + } + elseif (array_key_exists($attribute, Input::file())) + { + $line = 'file'; + } + else + { + $line = 'string'; + } + + return Lang::line("{$bundle}validation.{$rule}.{$line}")->get($this->language); + } + /** * Replace all error message place-holders with actual values. * @@ -592,16 +628,20 @@ protected function replace($message, $attribute, $rule, $parameters) if (in_array($rule, $this->size_rules)) { - // Even though every size rule will not have a place-holder for min, - // max, and size, we will go ahead and make replacements for all of - // them just for convenience. Except for "between", every replacement - // should be the first parameter. + // Even though every size rule will not have a place-holder for min, max, + // and size, we will go ahead and make replacements for all of them just + // for convenience. Except for "between", every replacement should be + // the first parameter in the array. $max = ($rule == 'between') ? $parameters[1] : $parameters[0]; $replace = array($parameters[0], $parameters[0], $max); $message = str_replace(array(':size', ':min', ':max'), $replace, $message); } + + // The :values place-holder is used for rules that accept a list of + // values, such as "in" and "not_in". The place-holder value will + // be replaced with a comma delimited list of the values. elseif (in_array($rule, $this->inclusion_rules)) { $message = str_replace(':values', implode(', ', $parameters), $message); @@ -613,18 +653,21 @@ protected function replace($message, $attribute, $rule, $parameters) /** * Get the displayable name for a given attribute. * - * Storing attribute names in the language file allows a more reader friendly - * version of the attribute name to be place in the :attribute place-holder. - * - * If no language line is specified for the attribute, a default formatting - * will be used for the attribute. - * * @param string $attribute * @return string */ protected function attribute($attribute) { - $display = Lang::line('validation.attributes.'.$attribute)->get($this->language); + $bundle = Bundle::prefix($this->bundle); + + // More reader friendly versions of the attribute names may be stored + // in the validation language file, allowing a more readable version + // of the attribute name to be used in the validation message. + // + // If no language line has been specified for the attribute, all of + // the underscores will be removed from the attribute name and that + // will be used as the attribtue name in the message. + $display = Lang::line("{$bundle}validation.attributes.{$attribute}")->get($this->language); return (is_null($display)) ? str_replace('_', ' ', $attribute) : $display; } @@ -658,9 +701,9 @@ protected function parse($rule) { $parameters = array(); - // The format for specifying validation rules and parameters follows - // a {rule}:{parameters} convention. For instance, "max:3" specifies - // that the value may only be 3 characters in length. + // The format for specifying validation rules and parameters follows a + // {rule}:{parameters} formatting convention. For instance, the rule + // "max:3" specifies that the value may only be 3 characters long. if (($colon = strpos($rule, ':')) !== false) { $parameters = explode(',', substr($rule, $colon + 1)); @@ -669,6 +712,20 @@ protected function parse($rule) return array(is_numeric($colon) ? substr($rule, 0, $colon) : $rule, $parameters); } + /** + * Set the bundle that the validator is running for. + * + * The bundle determines which bundle the language lines will be loaded from. + * + * @param string $bundle + * @return Validator + */ + public function bundle($bundle) + { + $this->bundle = $bundle; + return $this; + } + /** * Set the language that should be used when retrieving error messages. * @@ -687,7 +744,7 @@ public function speaks($language) * @param Database\Connection $connection * @return Validator */ - public function connection(\Laravel\Database\Connection $connection) + public function connection(Database\Connection $connection) { $this->db = $connection; return $this; @@ -698,14 +755,15 @@ public function connection(\Laravel\Database\Connection $connection) */ public function __call($method, $parameters) { - // First we will slice the "validate_" prefix off of the validator - // since customvalidators aren't registered with such a prefix. + // First we will slice the "validate_" prefix off of the validator since + // custom validators aren't registered with such a prefix, then we can + // just call the method with the given parameters. if (isset(static::$validators[$method = substr($method, 9)])) { return call_user_func_array(static::$validators[$method], $parameters); } - throw new \BadMethodCallException("Call to undefined method [$method] on Validator instance."); + throw new \Exception("Call to undefined method [$method] on Validator instance."); } } \ No newline at end of file diff --git a/laravel/view.php b/laravel/view.php index ee5bf2bc..870d32f3 100644 --- a/laravel/view.php +++ b/laravel/view.php @@ -1,8 +1,6 @@ - + * // Create a new view instance + * $view = new View('home.index'); + * + * // Create a new view instance of a bundle's view + * $view = new View('admin::home.index'); + * + * // Create a new view instance with bound data + * $view = new View('home.index', array('name' => 'Taylor')); + * + * * @param string $view * @param array $data * @return void @@ -45,15 +61,16 @@ public function __construct($view, $data = array()) $this->data = $data; $this->path = $this->path($view); - // If a session driver has been specified, we will bind an instance of - // the validation error message container to every view. If an errors - // instance exists in the session, we will use that instance. + // If a session driver has been specified, we will bind an instance of the + // validation error message container to every view. If an errors instance + // exists in the session, we will use that instance. // - // This makes the implementation of the Post/Redirect/Get pattern very - // convenient since each view can assume it has a message container. - if (Config::$items['session']['driver'] !== '' and IoC::core('session')->started()) + // This makes error display in the view extremely convenient, since the + // developer can always assume they have a message container instance + // available to them in the view. + if (Config::get('session.driver') !== '' and Session::started() and ! isset($this['errors'])) { - $this->data['errors'] = IoC::core('session')->get('errors', function() + $this->data['errors'] = Session::get('errors', function() { return new Messages; }); @@ -70,28 +87,33 @@ protected function path($view) { $view = str_replace('.', '/', $view); + $root = Bundle::path(Bundle::name($view)).'views/'; + + // Views may have the normal PHP extension or the Blade PHP extension, so + // we need to check if either of them exist in the base views directory + // for the bundle. We'll check for the PHP extension first since that + // is probably the more common of the two. foreach (array(EXT, BLADE_EXT) as $extension) { - if (file_exists($path = VIEW_PATH.$view.$extension)) + if (file_exists($path = $root.Bundle::element($view).$extension)) { return $path; } } - throw new \RuntimeException("View [$view] does not exist."); + throw new \Exception("View [$view] does not exist."); } /** * Create a new view instance. * - * The name of the view given to this method should correspond to a view - * within your application views directory. Dots or slashes may used to - * reference views within sub-directories. - * * * // Create a new view instance * $view = View::make('home.index'); * + * // Create a new view instance of a bundle's view + * $view = View::make('admin::home.index'); + * * // Create a new view instance with bound data * $view = View::make('home.index', array('name' => 'Taylor')); * @@ -106,16 +128,14 @@ public static function make($view, $data = array()) } /** - * Create a new view instance from a view name. - * - * View names are defined in the application composers file. + * Create a new view instance of a named view. * * - * // Create an instance of the "layout" named view - * $view = View::of('layout'); + * // Create a new named view instance + * $view = View::of('profile'); * - * // Create an instance of the "layout" view with bound data - * $view = View::of('layout', array('name' => 'Taylor')); + * // Create a new named view instance with bound data + * $view = View::of('profile', array('name' => 'Taylor')); * * * @param string $name @@ -124,68 +144,47 @@ public static function make($view, $data = array()) */ public static function of($name, $data = array()) { - if ( ! is_null($view = static::name($name))) - { - return static::make($view, $data); - } - - throw new \OutOfBoundsException("Named view [$name] is not defined."); + return new static(static::$names[$name], $data); } /** - * Find the key for a view by name. + * Assign a name to a view. * - * The view "key" is the string that should be passed into the "make" method and - * should correspond with the location of the view within the application views - * directory, such as "home.index" or "home/index". + * + * // Assign a name to a view + * View::name('partials.profile', 'profile'); * + * // Resolve an instance of a named view + * $view = View::of('profile'); + * + * + * @param string $view * @param string $name - * @return string + * @return void */ - protected static function name($name) + public static function name($view, $name) { - static::composers(); - - foreach (static::$composers as $key => $value) - { - if ($name === $value or $name === Arr::get((array) $value, 'name')) - { - return $key; - } - } + static::$names[$name] = $view; } /** - * Call the composer for the view instance. + * Register a view composer with the Event class. * - * @param View $view + * + * // Register a composer for the "home.index" view + * View::composer('home.index', function($view) + * { + * $view['title'] = 'Home'; + * }); + * + * + * @param string $view + * @param Closure * @return void */ - protected static function compose(View $view) + public static function composer($view, $composer) { - static::composers(); - - if (isset(static::$composers[$view->view])) - { - foreach ((array) static::$composers[$view->view] as $key => $value) - { - if ($value instanceof Closure) return call_user_func($value, $view); - } - } - } - - /** - * Load the view composers for the application. - * - * For better testing flexiblity, we load the composers from the IoC container. - * - * @return void - */ - protected static function composers() - { - if ( ! is_null(static::$composers)) return; - - static::$composers = require APP_PATH.'composers'.EXT; + Event::listen("composing: {$view}", $composer); } /** @@ -195,48 +194,75 @@ protected static function composers() */ public function render() { - static::compose($this); + // To allow bundles or other pieces of the application to modify the + // view before it is rendered, we will fire an event, passing in the + // view instance so it can modified by any of the listeners. + Event::fire("composing: {$this->view}", array($this)); - // All nested views and responses are evaluated before the main view. - // This allows the assets used by the nested views to be added to the - // asset container before the main view is evaluated and dumps the - // links to the assets. - foreach ($this->data as &$data) + $data = $this->data(); + + ob_start() and extract($data, EXTR_SKIP); + + // If the view is Bladed, we need to check the view for changes and + // get the path to the compiled view file. Otherwise, we'll just + // use the regular path to the view. + // + // Also, if the Blade view has expired or doesn't exist it will be + // re-compiled and placed in the view storage directory. The Blade + // views are re-compiled each time the original view is changed. + if (strpos($this->path, BLADE_EXT) !== false) { - if ($data instanceof View or $data instanceof Response) - { - $data = $data->render(); - } + $this->path = $this->compile(); } - ob_start() and extract($this->data, EXTR_SKIP); - - // If the view is Bladed, we need to check the view for modifications - // and get the path to the compiled view file. Otherwise, we'll just - // use the regular path to the view. - $view = (strpos($this->path, BLADE_EXT) !== false) ? $this->compile() : $this->path; - - try { include $view; } catch (\Exception $e) { ob_get_clean(); throw $e; } + try {include $this->path;} catch(\Exception $e) {ob_get_clean(); throw $e;} return ob_get_clean(); } /** - * Compile the Bladed view and return the path to the compiled view. + * Get the array of view data for the view instance. + * + * The shared view data will be combined with the view data for the instance. + * + * @return array + */ + protected function data() + { + $data = array_merge($this->data, static::$shared); + + // All nested views and responses are evaluated before the main view. + // This allows the assets used by nested views to be added to the + // asset container before the main view is evaluated and dumps + // the links to the assets into the HTML. + foreach ($data as &$value) + { + if ($value instanceof View or $value instanceof Response) + { + $value = $value->render(); + } + } + + return $data; + } + + /** + * Get the path to the compiled version of the Blade view. * * @return string */ protected function compile() { - // For simplicity, compiled views are stored in a single directory by - // the MD5 hash of their name. This allows us to avoid recreating the - // entire view directory structure within the compiled directory. - $compiled = STORAGE_PATH.'views/'.md5($this->view); + // Compiled views are stored in the storage directory using the MD5 + // hash of their path. This allows us to easily store the views in + // the directory without worrying about re-creating the entire + // application view directory structure. + $compiled = STORAGE_PATH.'views/'.md5($this->path); // The view will only be re-compiled if the view has been modified // since the last compiled version of the view was created or no // compiled view exists. Otherwise, the path will be returned - // without re-compiling. + // without re-compiling the view. if ( ! file_exists($compiled) or (filemtime($this->path) > filemtime($compiled))) { file_put_contents($compiled, Blade::compile($this->path)); @@ -254,9 +280,6 @@ protected function compile() * * // Equivalent functionality using the "with" method * $view = View::make('foo')->with('footer', View::make('partials.footer')); - * - * // Bind a view instance with data - * $view = View::make('foo')->nest('footer', 'partials.footer', array('name' => 'Taylor')); * * * @param string $key @@ -285,35 +308,49 @@ public function with($key, $value) } /** - * Magic Method for getting items from the view data. + * Add a key / value pair to the shared view data. + * + * Shared view data is accessible to every view created by the application. + * + * @param string $key + * @param mixed $value + * @return void */ - public function __get($key) + public static function share($key, $value) { - return $this->data[$key]; + static::$shared[$key] = $value; } /** - * Magic Method for setting items in the view data. + * Implementation of the ArrayAccess offsetExists method. */ - public function __set($key, $value) + public function offsetExists($offset) { - $this->with($key, $value); + return array_key_exists($offset, $this->data); } /** - * Magic Method for determining if an item is in the view data. + * Implementation of the ArrayAccess offsetGet method. */ - public function __isset($key) + public function offsetGet($offset) { - return array_key_exists($key, $this->data); + if (isset($this[$offset])) return $this->data[$offset]; } /** - * Magic Method for removing an item from the view data. + * Implementation of the ArrayAccess offsetSet method. */ - public function __unset($key) + public function offsetSet($offset, $value) { - unset($this->data[$key]); + $this->data[$offset] = $value; + } + + /** + * Implementation of the ArrayAccess offsetUnset method. + */ + public function offsetUnset($offset) + { + unset($this->data[$offset]); } /** @@ -326,25 +363,4 @@ public function __toString() return $this->render(); } - /** - * Magic Method for handling the dynamic creation of named views. - * - * - * // Create an instance of the "layout" named view - * $view = View::of_layout(); - * - * // Create an instance of a named view with data - * $view = View::of_layout(array('name' => 'Taylor')); - * - */ - public static function __callStatic($method, $parameters) - { - if (strpos($method, 'of_') === 0) - { - return static::of(substr($method, 3), Arr::get($parameters, 0, array())); - } - - throw new \BadMethodCallException("Method [$method] is not defined on the View class."); - } - } \ No newline at end of file diff --git a/public/bundles/.gitignore b/public/bundles/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/public/index.php b/public/index.php index 9b12abc2..d50d5845 100644 --- a/public/index.php +++ b/public/index.php @@ -3,7 +3,7 @@ * Laravel - A PHP Framework For Web Artisans * * @package Laravel - * @version 2.1.0 + * @version 2.0.7 * @author Taylor Otwell * @link http://laravel.com */ @@ -13,22 +13,37 @@ // -------------------------------------------------------------- define('LARAVEL_START', microtime(true)); +// -------------------------------------------------------------- +// Define the directory separator for the environment. +// -------------------------------------------------------------- +define('DS', DIRECTORY_SEPARATOR); + // -------------------------------------------------------------- // The path to the application directory. // -------------------------------------------------------------- -$application = '../application'; +define('APP_PATH', realpath('../application').DS); + +// -------------------------------------------------------------- +// The path to the bundles directory. +// -------------------------------------------------------------- +define('BUNDLE_PATH', realpath('../bundles').DS); + +// -------------------------------------------------------------- +// The path to the storage directory. +// -------------------------------------------------------------- +define('STORAGE_PATH', realpath('../storage').DS); // -------------------------------------------------------------- // The path to the Laravel directory. // -------------------------------------------------------------- -$laravel = '../laravel'; +define('SYS_PATH', realpath('../laravel').DS); // -------------------------------------------------------------- // The path to the public directory. // -------------------------------------------------------------- -$public = __DIR__; +define('PUBLIC_PATH', realpath(__DIR__).DS); // -------------------------------------------------------------- // Launch Laravel. // -------------------------------------------------------------- -require $laravel.'/laravel.php'; \ No newline at end of file +require SYS_PATH.'laravel.php'; \ No newline at end of file diff --git a/storage/cache/.gitignore b/storage/cache/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/application/storage/database/.gitignore b/storage/database/.gitignore similarity index 100% rename from application/storage/database/.gitignore rename to storage/database/.gitignore diff --git a/storage/logs/.gitignore b/storage/logs/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/storage/sessions/.gitignore b/storage/sessions/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/storage/views/.gitignore b/storage/views/.gitignore new file mode 100644 index 00000000..e69de29b