nova-framework / framework

Demo application using Nova - this is an extensive playground, use "app" repository for real applications.
http://novaframework.com/
MIT License
418 stars 210 forks source link

SMVC restructure #222

Closed dcblogdev closed 8 years ago

dcblogdev commented 8 years ago

I've given the framework a bit of a restructure so it main files are outside of the web root by default and the only files that should be inside the web root are the templates, .htaccess and index.php.

so the structure is now like this:

app
    Controllers
    Core
    Helpers
    Language    
    logs
        error.log
    Models
    Modules
    vendor
    views
    composer.json
public_html
    templates
        default
    .gitignore
    .htaccess
    index.php
    licence.txt

From the index.php the starting path would be:

$smvc = '../';

The vendor path would be

if (file_exists(SMVC.'app/vendor/autoload.php')) {
    require SMVC.'app/vendor/autoload.php';
}

I've also adjusted the view class and url helper to make the paths more flexible.

This would mean all public files (templates) would be in the web root all classes would be outside the web root.

The logger class has been changes since it would no longer be able to serve html errors they would be stored in app/logs/error.log above the document root.

This kind of setup needs to happen, it's a basic security practice. I'm very late in changing this.

First off is there any objections to this?

Additionally should this be a sub part update 2.2.1 or warrant a full version increase? personally I think 2.2.1 would be sufficient no new functionality has been added.

Another area I'm not 100% on is Modules. They may need to be in the web root as well so any modules containing css can be loaded but at the same time they will contain classes so should be above the root! will require some thought on this one.

I haven't pushed this up yet I'm waiting to get your thoughts on this.

nhymxu commented 8 years ago

Of course, you can agree or not.

And I don't see CI put config file in system. CI storage all config file in application/config folder.

dcblogdev commented 8 years ago

I just don't see what benefit it would give, when I looked at this repo it's in the system https://github.com/bcit-ci/CodeIgniter/blob/develop/system/core/Config.php

geomorillo commented 8 years ago

Dave that Config is diferent from the one inside CodeIgniter/application/config/config.php Still i think the config.php in smvc is fine where it is, because is not reallly a part of the implementation. We can argue a lot about this, but i agree with Dave.

dcblogdev commented 8 years ago

Ah I stand corrected :)

jimgwhit commented 8 years ago

Why would anyone complain about where @daveismyname put the config file really. Folks system is just another folder name config is fine there.

jimgwhit commented 8 years ago

I guess you are right Dave. It's hard to please everyone. And I shutter to ask but why are we comparing and worrying about the config in CodeIgniter verses SMVC?

nhymxu commented 8 years ago

I talk about where config file locate because I see image about folder structure https://github.com/simple-mvc-framework/framework/issues/222#issuecomment-161915395

Multi app need multi config file. Using only same system (core) framework.

This structure like advanced template in Yii2.

P/s: Sorry, image using system on each project folder. My mistake.

dcblogdev commented 8 years ago

I understand where you're coming from now, but to have a multiple app it needs more then the config also the languages folder if it's using language files, routes file, error.log.

I hadn't considered sharing the system folder across multiple projects. I'm open to suggestions should this be a consideration?

jimgwhit commented 8 years ago

@daveismyname really shouldn't each site be self-contained and not shared, that's just more efficient. If you start using a folder for multiple sites mass confusion can happen. And can't anyone figure out how to do this stuff on their own by pointing files to a certain place people please. And again why is it being compared to Yii. If someone really wants those features enough, why don't they just use Yii. SMVC is not Yii. Golie wiligers.

jimgwhit commented 8 years ago

@nhymxu you said

Multi app need multi config file. Using only same system (core) framework

If you carefully look at the folder structure in the second video Dave did you can point to a common system folder already just by playing with the

../../whatever/system

You're not thinking things through clearly. And you're kinda making things harder than they really need to be.

jimgwhit commented 8 years ago

And the yek with Yii, we are talking about SMVC.

jimgwhit commented 8 years ago

@nhymxu And another thing SMVC it's not that big you could have 10 sites with 10 different system folders and that would take up less room than a single installation of Yii or laravel alone, good gracious me has anyone even thought that much out yet? Sometimes you can really over think something and complicate it instead of making it simple and easy. I beg you stop the nonsense. Or just use Yii.

jimgwhit commented 8 years ago

@nhymxu if we want to compare frameworks let's compare SMVC to Laravel, the way Dave has set up version 3.0 beta is like laravel, they also require separate sites and not shared folders, Laravel is the number one framework in the world, and Dave has set up deployment like laravel, so can we please leave things alone?

geomorillo commented 8 years ago

My opinion: I think multi app setup would complicate things a little, maybe for now we should not consider this.

jimgwhit commented 8 years ago

@daveismyname I know I have no say so, but please don't do the shared thing, please have each site requires its own SMVC installation, that just makes sense, and will avoid mass confusion.
And I'm sure if @nhymxu needs help setting up a special case you would help him, I know you have gone out of your way to help me, and I thank you very very much

jimgwhit commented 8 years ago

Thank you @geomorillo for coming in and sharing your thoughts too.

woodsy1 commented 8 years ago

@jimgwhit you are spaming this thread with unnecessary comments and having a go at people and their questions. You have had your time and your help from Dave it is only fair you let others have their chance too (even if you disagree with their questions)

If you don't have some constructive contribution to make perhaps it's best if you back off for a while! On Dec 8, 2015 4:55 AM, "jimgwhit" notifications@github.com wrote:

Thank you @geomorillo https://github.com/geomorillo for coming in and sharing your thoughts too.

— Reply to this email directly or view it on GitHub https://github.com/simple-mvc-framework/framework/issues/222#issuecomment-162607455 .

jimgwhit commented 8 years ago

@woodsy1 if some of the people are so concerned about codeigniter and Yii, why aren't they at those websites. My concern is SMVC which I use in real live production sites. And yes I tend to get concerned when some folks want to hack it up and bloat it up.
And if you read the comments carefully you will see that I gave a suggestion on how to work around a problem the individual is having, without changing the framework further.
I mean for crying out loud, that folder structure that that individual wants could be customized, do we really expect Dave to write the framework to suit 500 different special needs, when people should roll up their sleeves and program their own customizations.

woodsy1 commented 8 years ago

@jimgwhit Thats fine but the manner in which you are talking to people here is not acceptable.

jimgwhit commented 8 years ago

@woodsy1 I realize I can come across a little strong, but think about it someone is asking a question that they could do themselves with a little bit of ingenuity. I guess it bothers me when someone expects the framework to be customized 500 different ways when that should be done by the user / programmer.

woodsy1 commented 8 years ago

Again you have asked questions on this thread earlier before you had Dave helped you that I thought were simple but I did not have a go at you or your questions. It is best unless you can help someone constructively and politely that you leave Dave to handle the questions.

jimgwhit commented 8 years ago

@woodsy1 well I won't go on and on I promise, but let me leave it at this my question was on current usage in the actual current framework. Not a change that someone could customize them self as already clearly stated. And yes I will leave it at that.
And @woodsy1 please stop and think for one minute, do you really want customer A's routes really mixed in with customer B's routes if so all I can say is WOW. And I ask that because from the sound of things here it seems you want what @nhymxu wants.

woodsy1 commented 8 years ago

I have discovered a nice way of keeping your project completely self contained on local to allow you to put the entire thing in a git repo using virtual hosts. I will post a tutorial on it on the forums.

dcblogdev commented 8 years ago

@woodsy1 oh nice I will keep an eye out for that.

nhymxu commented 8 years ago

I see @jimgwhit hate me? Sure?

I don't think my problem is multi customer in one system?

You build 2 system for 2 customer in 1 shared hosting? I think you don't know what I say???

Stop disscuss about my problem. It's rejected

jimgwhit commented 8 years ago

I don't hate you, all I was saying is it's easier with multi sites.

LuckyCyborg commented 8 years ago

Hello, some thoughts about SMVC restructuring.

First of all, I should say that I arrived to SMVC because because was choose by our team as a base framework to port a massive and complex e-Learning Portal, being monolithic/old-style, to HMVC (think something about 90MB source code), while maximizing the future application response speed, for supporting a maximum users on-line possible. Sure, SMVC being a Micro-Framework, excelled in this field (4-6 times faster than the well acclaimed CodeIgniter, on the tests), and its (incipient) Modules support and namespaces was seen as advantages.

Before someone to badmouth the Modules, I should say that using a Modular approach, hugely simplify the work in a team and permit to develop different combinations of facilities. Please remember that I look to SMVC as a code base for a literally huge Portal, being developed by 10-12 programmers, and not as to develop some rather simple blog or one-page presentation website...

Then, some critics and suggestions for SMVC 3.0.

First of all, namespace pollution and system/app isolation. I consider a wise decision to use name space 'App' for the application components, isolating them. Yet, we should remember that SMVC is not alone in the PHP (and Composer) Ecosystem. While, for a simple presentation site, using '\Core' and '\Helpers' will glorious works, to create some more complex site we will need to use another (Composer) packages. In other hand, in Composer World, anonymous (as in non-product) namespaces are considered being for local implementations.

So, my suggestion is to apply a name space also to 'system' path components. Us, we use a generic one: '\MVC', maybe Dave will choose a more proud one, i.e: '\Smvc', but in my opinion, the SMVC components should be enclosed in well defined namespaces. Is not complicated and simplify the life. Plus, think about the Proud Moment while reading:

 class Controller extend \Smvc\Core\Controller

Secondly, no Events and EventManager, at all? Come-on! An EventManager cam be written in two classes and around 100 lines of code, even if you do not want to reuse available components, i.e. from Symfony.

The Events are of fantastic utility, even for simple sites, if you don't want to write again and write again same code! Simple example: you have to develop two sites, but one should send an welcome mail, while another not, on user registration. Using Events, the only difference in code between those sites, is that in first one, a Mailer class watch the User Registration Events, and do its job.

In other hand, the Events are, well handily when you work with Modules. You can use them to extend the functionality of some Modules, using another Modules, etc.

Thirdly, right now, SMVC configuration is literally rudimentary. It have a class who do in its constructor some defines and initialization. That's all. Sure, for a simple blog maybe it is enough, but when complexity increase, the needed configuration elements increase too. Also, some Modules need specific configuration too.

One can argue that we can add all the "defines" in that today class. Well, it is a POV, but we miss right on the Modules idea: self-contained components. That's why I suggest that SMVC to do some real configuration, supporting Modules-side too. And it is not complicated. For example, we use:

namespace Core;

class Config {

    protected static $settings = array();

    public static function get($key) {
        return isset(self::$settings[$key]) ? self::$settings[$key] : null;
    }

    public static function set($key, $value) {
        self::$settings[$key] = $value;
    }

} 

And, then, all is settled in some files in app/Config directory:

app/Config/bootstrap.php app/Config/config.php app/Config/constants.php app/Config/routes.php

The file names I think that are self-explanatory. A very simple class like this one is very handily, because it permit even to have arrays as configuration parameters, instead of simple defines. In plus, there is a class called Modules, which walk on Config directories of every active module and load their configuration.

And talking about Configuration files, I believe that them should not stay in system/core, instead to have their dedicated directories into App and Modules. For example: 'app/Config', because they are specific to application.

Another argument is that in the SMVC's system directory shall be only the shipped SMVC product, which I touch only when I do an update because Dave do an update. All configuration, all logs, all customization should go on application directory, aka 'app'. Why?

Because in this way you can develop 1000 applications, every one different, with different databases, responding on different domains, while using ONE SMVC SYSTEM installation. And that's very handily for security updates. And yes, even in Shared Hosting this thing is possible, i.e. when using Hostgator.

Fourthly, I would like to argue about SMVC's Routing. Sure, is nice and fancy, but I consider very nice to have ability to easily extend the \Core\Router. For example, to support the Files Serving. What is file serving? Let's look at SMVC 3.0 tree. We see that (by default?) it use a public directory as webroot. A very security wise decision which I salute, but having the framework behind the webroot, we hit a problem: the Assets Files aren't available directly, being behind the Web accessible directories.

One can move those Asset Files on webroot. Yeah, can be a solution, but not most lucky one. You have to move the Asset Files, the Templates Asset Files, and what you do about Modules Asset Files? Move even them? But, like I said, the Modules shall be self-contained, right?

A much more simple solution for this problem is Asset Files Serving via Router. Using some _pregmatch, in URI dispatching, the Router can detect the Asset URIs and call a simple method which is a classic file downloader, rather that looking for matching Route. It is complicated? Nope. Look at this functional example:

    /**
     * Runs the callback for the given request.
     */
    public static function dispatch()
    {
        // Detect the URI and the HTTP Method.
        $uri = Url::detectUri();

        $method = Request::getMethod();

        // For properly Assets serving, the file URI should be as following:
        //
        // /templates/default/assets/css/style.css
        // /modules/blog/assets/css/style.css
        // /assets/css/style.css

        if (Request::isGet()) {
            $filePath = '';

            if(preg_match('#^assets/(.*)$#i', $uri, $matches)) {
                // In protected Assets path.
                $filePath = 'assets/'.$matches[1];
            }
            else if (preg_match('#^(templates|modules)/(.+)/assets/(.*)$#i', $uri, $matches)) {
                // We need to classify the second match string (the Module/Template name).
                $moduleName = Inflector::classify($matches[2]);

                $filePath = $matches[1].'/'.$moduleName.'/Assets/'.$matches[3];
            }

            if(! empty($filePath)) {
                self::serveAsset($filePath);

                return;
            }
        }

        // Routes the current request.

        foreach (self::$routes as $route) {
            if ($route->match($uri, $method)) {
                // Found a valid Route; invoke the Route's Callback and go out.
                self::invokeObject($route->callback(), $route->params());

                return;
            }
        }

        // No valid Route found; invoke the Error Callback with the current URI as parameter.
        $params = array(
            htmlspecialchars($uri, ENT_COMPAT, 'ISO-8859-1', true)
        );

        self::invokeObject(self::$errorCallback, $params);
    }

    public static function serveAsset($filePath)
    {
        $expires = 60 * 60 * 24 * 365; // Cache for one year

        // Make the filePath absolute.
        $filePath = realpath(BASEPATH .$filePath);

        if (! file_exists($filePath)) {
            header("{$_SERVER['SERVER_PROTOCOL']} 404 Not Found");

            return false;
        }
        else if (! is_readable($filePath)) {
            header("{$_SERVER['SERVER_PROTOCOL']} 403 Forbidden");

            return false;
        }
        //
        // Collect the current file information.

        $finfo = finfo_open(FILEINFO_MIME_TYPE); // Return mime type ala mimetype extension

        $contentType = finfo_file($finfo, $filePath);

        finfo_close($finfo);

        // There is a bug with finfo_file();
        // https://bugs.php.net/bug.php?id=53035
        //
        // Hard coding the correct mime types for presently needed file extensions
        switch($fileExt = pathinfo($filePath, PATHINFO_EXTENSION)) {
            case 'css':
                $contentType = 'text/css';
                break;
            case 'js':
                $contentType = 'application/javascript';
                break;
            default:
                break;
        }

        //
        // Prepare and send the headers with browser-side caching support.

        // Get the last-modified-date of this very file
        $lastModified = filemtime($filePath);

        // Get a unique hash of this file (etag)
        //$etagFile = md5_file($filePath);

        // Get the HTTP_IF_MODIFIED_SINCE header if set
        $ifModifiedSince = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? $_SERVER['HTTP_IF_MODIFIED_SINCE'] : false;

        // Get the HTTP_IF_NONE_MATCH header if set (etag: unique file hash)
        //$etagHeader = isset($_SERVER['HTTP_IF_NONE_MATCH'] ? trim($_SERVER['HTTP_IF_NONE_MATCH']) : false;

        // Firstly, we finalize the output buffering.
        if (ob_get_level()) ob_end_clean();

        header('Access-Control-Allow-Origin: *');
        header('Content-type: ' .$contentType);
        header('Expires: '.gmdate('D, d M Y H:i:s', time() + $expires).' GMT');
        header('Last-Modified: '.gmdate('D, d M Y H:i:s', $lastModified).' GMT');
        //header('Etag: '.$etagFile);
        header('Cache-Control: max-age='.$expires);

        // Check if page has changed. If not, send 304 and exit
        //if ((@strtotime($ifModifiedSince) == $lastModified) || ($etagHeader == $etagFile)) {
        if (@strtotime($ifModifiedSince) == $lastModified) {
            header("{$_SERVER['SERVER_PROTOCOL']} 304 Not Modified");

            return true;
        }

        //
        // Send the current file.

        header("{$_SERVER['SERVER_PROTOCOL']} 200 OK");
        header('Content-Length: ' .filesize($filePath));

        // Send the current file content.
        readfile($filePath);

        return true;
    }

Sure, you will notice that we use a "custom" class, called Route and to note that auto-dispatching is not gone (its work is done in Route). Yep, we do it to simplify the code, and we do not do huge complications compared with the orthodox SMVC. As idea, the Files Serving is in the first part of this method. Also, to note that serveAsset method looks complicated because we try to implement the right browser-side caching protocol (someone can want to implement also bytes range support, too); in reality, all what is needed for having a functional file downloader is the first and last part of method's code.

That File Serving even permit to condition the files access on different situations (user logged in, time limit or traffic limit, etc).

Lastly, I consider that will be very nice if SMVC do something about the salad of capitalized and lowercase directories. Sure, some needs to be capitalized because composer walk on them. But, why not use all directories capitalized? We have advantages to use views instead of Views? Nope. I for one I believe that using, like I said, the all Capitalized Directories structure give a consistent look, and even simplify the developers life.

As framework tree, we believe that the best tree is something like:

app
    Config
    Controllers
    Helpers
    Language
    Logs
    Models
    Views
assets
    css
    files
    img
    js
modules
    <Module1> 
        Assets
            css
            img
            js
        Config
        Controllers
        Helpers
        Language
        Models
        Views        
    <Module2> 
        Assets
            css
            img
            js
        Config
        Controllers
        Helpers
        Language
        Models
        Views  
templates
    Admin
        Assets
            css
            img
            js
        Language
    Default
        Assets
            css
            img
            js
        Language
tmp
    cache
system
    Core
    Helpers
    Language
    Utils
webroot

To note that literally we use this directories structure right now, for the single app style. For multiple apps with one system directory, we move the root directories: Assets, Modules and Templates, into App tree.

Finally, hopping that my critics and suggestions are somewhat useful, a sincere

All the Best!

jimgwhit commented 8 years ago

@LuckyCyborg thank you so so so so so much, you just made my main point convincing that someone could take the framework as is and custom modify it to suit their needs. So let's not flim flam up the framework but let people do what you have done download it and custom modify it to suit your particular needs. @daveismyname , like I have mentioned this framework is supposed to be a smaller to medium scale simple framework if someone wants all the big humongous giant features why don't they use laravel or zend? But someone who is advanced could take the framework as downloaded and modify it to suit their sites needs, they could even scale it for a heavy duty website if need be. If advanced enough someone could even write their own router. I guess the bottom line is there's a way to keep it simple but yet advanced programmer could do enough modifications to scale it however need be. So why does some people think it has to be monkied with so much?

nhymxu commented 8 years ago

@LuckyCyborg I'm feeling excited about your project. Can you share your core project code? Thanks you!

jimgwhit commented 8 years ago

And to try to put this in a nice way let's say,

Joe wants feature A
Billy Bob wants feature B
Cindy wants feature C

Couldn't they install the framework, and custom write their own classes and customize in those features they want themselves? Billy Bob might not want what Cindy needs, Joe Blow may not need what Rodriguez needs, Sue Bob may not need any of it. So hopefully this explains, it is a framework that a person can customize themselves to suit their own needs without putting junk in there that someone else does not need or want.
I mean can't we keep the basic downloadable framework kind of simple but yet a person with programming skill can modify it as needed without forcing what they need on someone else who does not need that. And I am so so sorry if someone does not understand that.

tomvlk commented 8 years ago

I really want to demonstrate the Service layer that I mentioned earlier (pretty far scroll distance actually :chicken:)

Demo on Service Layer

To demonstrate how easy it is to not use SQL anymore for the most basic CRUD operations, and even not write it for models too I made a branch on my fork: https://github.com/tomvlk/php-framework/tree/demo-service-layer

You can clone and run it (note: it's on 3.0). Database export is also included!

How?

In the Demo controller you can see the logic of getting something from the database. Just basicly doing this will get you the car with id 1:

$carService = new \App\Services\CarService();
$car = $carService->get(1);

And because the car has a relation to the 'Make' model you can map the relation with a lazy getter inside of the model Make:

Source of the lazy getter inside of car to get the make object that is coupled with the car:

public function getMake() {
    // Will check if make is already 'cached' in this model. If not, then fetch it!
    if ($this->make == null) {
        // Fetch Make object
        $service = new MakeService();
        $this->make = $service->get($this->makeid);
    }

    // Return the make object
    return $this->make;
}

With this lazy getter you can call the getMake() at any time on your returned Car model from the database. This will only get the make that has a relationship from car when you call getMake() for the first time. When using the getMake() after the first time it will use cached make that is declared in the model Car.

$carService = new \App\Services\CarService();
$car = $carService->get(1);

$make = $car->getMake();

Why

Better maintainability, less SQL inside of your model, because model should be only data in my opinion, and no database getters (that is actually the service layer in the current 2.* version).

Hard to explain it without showing you the whole code So please take a look at the source here: https://github.com/tomvlk/php-framework/tree/demo-service-layer

jimgwhit commented 8 years ago

@tomvlk you further emphasize my point that someone could take the framework as is and modify it to suit their own needs, how come some people just don't understand that. And again trying to be nice here I disagree, the model is where the database logic goes. @daveismyname done this correctly. And @daveismyname has said you can use another database provider such as eloquent if you choose. And unless you're changing data getters and setters are unnecessary, and again if you need a getter settter class simply write one.

tomvlk commented 8 years ago

@jimgwhit take it easy I just explained my idea. It seems you feel attacked by it. And didn't say anything about doing something wrong here. It's how you are building the system.

It's more like, look this could be a great style on how to create big applications when using much db logic and you want to seperate it from the objects that it will be mapped in. And someone asked how that would look like, so please take it easy!

dcblogdev commented 8 years ago

@LuckyCyborg thanks for this I'm going to read through this again so I understand your points before commenting on them.

@tomvlk thank you I did ask you about this I'll have a look at your repo.

@jimgwhit everyone know you're reluctant to change and if you had your way the framework would never change in any way, you've made your views extremely clear please give others the chance to do the same, commenting as you do may put other people off from expressing their ideas.

At this point I'm not ruling anything out let's get the ideas out there see what's useful and what's not

geomorillo commented 8 years ago

Yes @jimgwhit take it easy he is just proposing some ideas, i think they are interesting , also remember that every software has to evolve at some point, or should we stay only with the 2.x branch forever?, you can always fork your own copy and do your own version, i have one myself, with my own modifications and they work really great.

jimgwhit commented 8 years ago

@geomorillo and @daveismyname it was mentioned above,
you can always fork your own copy and do your own version, i have one myself, with my own modifications and they work really great.
Ok that is all I am trying to say if I can fork my own why can't they fork their own and modify as need be.
I have no problem with people suggesting things, but some are wanting to add things that is only highly specialized to meet their needs only, which should be a customized thing and not a cast in stone thing.

tomvlk commented 8 years ago

@jimgwhit Service layer isn't specialized, it's a well known implementation. But anyway, if we want to build 3.0 we should look into some new things, and if it is worth it.

jimgwhit commented 8 years ago

@tomvlk would it be required to use the service layer? Could I still use things the regular way even if the framework had the service layer included?

dcblogdev commented 8 years ago

but every time someone comments you come along and comment to say they should not be doing that, it's going to lead to less confident people to not comment at all.

Some suggestions are for things I'm not aware off lets take advantage of each other's knowledge we as individuals don't know everything.

If things can be useful for wide audiences then that's got to be a good things, by looking at the services for instance that add functionality but does not take away the current style of writing code it's a choice to use it or not.

jimgwhit commented 8 years ago

@daveismyname I have absolutely no problem with the additions if like you said they are optional.

geomorillo commented 8 years ago

I think they are optional @jimgwhit, i would prefer them to be optional, but they are really interesting worth looking at.

dcblogdev commented 8 years ago

it's worth looking at the options then looking at implementations they don't necessarily end up in the framework but lets not limit ourselves right away

ghost commented 8 years ago

@tomvlk Thanks for sharing your code in regards to the service layer, I was quite interested in how it would have worked. :smile:

LuckyCyborg commented 8 years ago

@jimgwhit Thanks for your kind words, but when someone completely re-implement the Router, Controller, View (with auto-detection of Views Paths and auto-rendering) and Language (with some INTL and ICU taste) classes, introducing Templates as in Themes with Layouts and Fragments, unique language files support for System, App, Modules and Templates, change the configuration mode using a new class (Config) as base, the framework structure and initialization (like described with bootstrap, config, constants & Co.), when even its namespaces are changed, that can be barely called... customization.

Instead, I believe that is it called eventually fork, even if it is not public, yet. Practically, I can say that The Thing what we developed and use is a different animal, which is and behave different to SMVC. The main problem is that we have no intention to fork SMVC; that's WHY we try to contribute back some of our modifications and enhancements, hopping that using common sense and small changes, SMVC, in a future version, maybe can accommodate our requirements, without us to rewrite its Core Components.

Yet, you should do not worry, we have no intention to ask Dave to write the Second Laravel, CakePHP or CodeIgniter, being more than happy to use the original things when is the case, still having a project with specific requirements, which ask us to use SVMC for implementation.

And, I for one, I believe that simple is not another word for rudimentary, instead we all can have a simple and smart framework. Our suggestions aren't of nature to make SMVC fat or complicated, take them as small (and maybe smart) enhancements.

@nhymxu Well, like I said, we have no intention to fork SMVC, but, in future, if there is enough interest, there will be no problem to publish the Project's Core as an add-on on top of SVMC. My colleagues look even with enthusiasm to this idea and they say to be willing to follow the Core Framework with a companion application, a simple but complete CMS, to demonstrate how the things works. Yet, like I described in this post, do not expect to feel SMVC working on top of our solution. Maybe some taste of CakePHP.

Also, I say again, we have no intention to fork SVMC and our Core Framework will be published only as solution on top of SVMC, no another way.

@tomvlk Thanks for sharing your code! Nice take on Services design!

But if we do Services, lets do them right, in the sense that a Service usually is about collecting and processing data; not necessarily from database. For example, a service can be also a Twiter (O)Auth, or even a SSO component, which talk with a Single Sign On Server to identify if the current User is logged in or not. Is it your implementation generic enough to support even those types of Services?

@daveismyname I wait your comments and questions. I'm more than happy to give supplementary details.

jimgwhit commented 8 years ago

I actually have one project I did in laravel and even there on their forums sometimes they talk about repository patterns and services. However you can completely use laravel without having a repository pattern or a service, if you don't believe me download and install laravel 5.1 you can completely do database operations just using the querybuilder with no specialized services or repository patterns they are completely optional.
I don't think that is too much to ask.

LuckyCyborg commented 8 years ago

@jimgwhit At a frugal look on those Services, no one force you to use them. If you like the ol'good Database/Model style, those classes are always there for you, nothing is changed. In fact, the Services are complementary to the "classic" Model style, offering some supplementary facilities.

LuckyCyborg commented 8 years ago

OK, then my suggestion to enhance the Routing in the SVMC 3.0; offering a better support to further enhance the Router, if is the case.

Additional method on \Helpers\Request

    /**
     * Gets the request method.
     *
     * @return string
     */
    public static function getMethod() {
        $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET';

        if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
            $method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
        }
        elseif (isset($_REQUEST['_method'])) {
            $method = $_REQUEST['_method'];
        }

        return strtoupper($method);
    }

Additional method to \Helpers\Url

    public static function detectUri()
    {
        $requestUri = $_SERVER['REQUEST_URI'];
        $scriptName = $_SERVER['SCRIPT_NAME'];

        $pathName = dirname($scriptName);

        if (strpos($requestUri, $scriptName) === 0) {
            $requestUri = substr($requestUri, strlen($scriptName));
        }
        else if (strpos($requestUri, $pathName) === 0) {
            $requestUri = substr($requestUri, strlen($pathName));
        }

        if (($requestUri == '/') || empty($requestUri)) {
            return '/';
        }

        $uri = parse_url($requestUri, PHP_URL_PATH);

        return str_replace(array('//', '../'), '/', ltrim($uri, '/'));
    }

A new class, \Core\Route

namespace Core;

/**
 * The Route class is responsible for routing an HTTP request to an assigned callback function.
 * The Router tries to match the requested URL against a series of URL patterns.
 */
class Route
{
    /**
     * @var string HTTP method or 'ANY'
     */
    private $method;

    /**
     * @var string URL pattern
     */
    private $pattern;

    /**
     * @var mixed Callback
     */
    private $callback = null;

    /**
     * @var array Route parameters
     */
    private $params = array();

    /**
     * @var string Matching regular expression
     */
    private $regex;

    /**
     * Constructor.
     *
     * @param string $method HTTP method
     * @param string $pattern URL pattern
     * @param mixed $callback Callback function
     */
    public function __construct($method, $pattern, $callback)
    {
        $this->method   = $method;
        $this->pattern  = $pattern;
        $this->callback = $callback;
    }

    /**
     * Checks if a URL and  HTTP method matches the Route pattern.
     *
     * @param string $uri Requested URL
     * @param string $method HTTP method
     * @return boolean Match status
     */
    public function match($uri, $method)
    {
        if (($this->method != $method) && ($this->method != 'ANY')) {
            return false;
        }

        // Wildcard or exact match.
        if (($this->pattern === '*') || ($this->pattern === $uri)) {
            return true;
        }

        $last_char = substr($this->pattern, -1);

        // Build the regex for matching.
        if (strpos($this->pattern, ':') !== false) {
            $regex = str_replace(
                array(':any', ':num', ':all', ':hex', ':uuidV4'),
                array('[^/]+', '-?[0-9]+', '.*', '[[:xdigit:]]+', '\w{8}-\w{4}-\w{4}-\w{4}-\w{12}'),
                $this->pattern
            );
        }
        else {
            $regex = $this->pattern;
        }

        $regex = str_replace(array('(/', ')', '/*'), array('(?:/', ')?', '(/?|/.*?)'), $regex);

        // Fix trailing slash.
        if ($last_char === '/') {
            $regex .= '?';
        }
        // Allow trailing slash.
        else {
            $regex .= '/?';
        }

        // Attempt to match the Route and extract the parameters.
        if (preg_match('#^' .$regex .'(?:\?.*)?$#i', $uri, $matches)) {
            // Remove $matched[0] as [1] is the first parameter.
            array_shift($matches);
            // Store the extracted parameters.
            $this->params = $matches;
            // Also, store the compiled regex.
            $this->regex = $regex;

            return true;
        }

        return false;
    }

    //
    // Some Getters

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

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

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

}

The (rewritten) class \Core\Router

namespace Core;

use Core\Route;
use Helpers\Request;
use Helpers\Url;

/**
 * Router class will load requested controller / closure based on url.
 */
class Router
{
    private static $instance;

    /**
     * Array of routes
     *
     * @var array $routes
     */
    private $routes = array();

    /**
     * Set an Error Callback
     *
     * @var null $errorCallback
     */
    private $errorCallback = '\Core\Error@index';

    // Constructor
    public function __construct()
    {
        self::$instance =& $this;
    }

    public static function &getInstance()
    {
        if(! self::$instance) {
            $appRouter = APPROUTER;

            self::$instance = new $appRouter();
        }

        return self::$instance;
    }

    /**
     * Defines a route with or without Callback and Method.
     *
     * @param string $method
     * @param array @params
     */
    public static function __callStatic($method, $params)
    {
        $router = self::getInstance();

        $router->addRoute($method, $params[0], $params[1]);
    }

    /**
     * Defines callback if route is not found.
     *
     * @param string $callback
     */
    public static function error($callback)
    {
        $router = self::getInstance();

        $router->callback($callback);
    }

    public function callback($callback = null)
    {
        if(is_null($callback)) {
            return $this->errorCallback;
        }

        $this->errorCallback = $callback;
    }

    /**
     * Maps a Method and URL pattern to a Callback.
     *
     * @param string $method HTTP metod to match
     * @param string $pattern URL pattern to match
     * @param callback $callback Callback object
     */
    public function addRoute($method, $route, $callback)
    {
        $method = strtoupper($method);
        $pattern = ltrim($route, '/');

        $route = new Route($method, $pattern, $callback);

        array_push($this->routes, $route);
    }

    /**
     * Invoke the callback with its associated parameters.
     *
     * @param  object $callback
     * @param  array  $params array of matched parameters
     * @param  string $message
     */
    protected function invokeObject($callback, $params = array())
    {
        if (is_object($callback)) {
            // Call the Closure.
            return call_user_func_array($callback, $params);
        }

        // Call the object Controller and its Method.
        $segments = explode('@', $callback);

        $controller = $segments[0];
        $method     = $segments[1];

        // Initialize the Controller
        $controller = new $controller();

        // Execute the Controller's Method with the given arguments.
        return call_user_func_array(array($controller, $method), $params);
    }

    public function dispatch()
    {
        // Detect the URI and the HTTP Method.
        $uri = Url::detectUri();

        $method = Request::getMethod();

        foreach ($this->routes as $route) {
            if ($route->match($uri, $method)) {
                // Found a valid Route; invoke the Route's Callback and go out.
                $this->invokeObject($route->callback(), $route->params());

                return true;
            }
        }

        // No valid Route found; invoke the Error Callback with the current URI as parameter.
        $params = array(
            htmlspecialchars($uri, ENT_COMPAT, 'ISO-8859-1', true)
        );

        $this->invokeObject($this->callback(), $params);

        return false;
    }

}

The new \Core\Router class is compatible as configuration with the standard one, offering also the ability to use optional parameters in patterns, so will work a pattern like:

blog/archive(/:num(/:num(/:num)))

This example pattern will match the URIs:

/blog/archive/2015/12/31
/blog/archive/2015/12
/blog/archive/2015
/blog/archive

To note that for a properly work, this new Router need a define in Config class,

/**
 * Set the Application Router.
 */
define('APPROUTER', '\Core\Router');

and a slightly different approach in /public/index.php

// We should get the Router instance before to start Routes configuration.
$router = Router::getInstance();

// Load the Routes
include SYSTEM .'Core/routes.php';

// Execute matched routes
$router->dispatch();

Also, there is no fallback switch and no autoDispatch.

You'll notice that the new \Core\Router is very different from the orthodox one, and use only few static functions. But the best of the bests is that class can be easily extended; for example, if we want a Classic Router, running in ol'good style of AutoDispatching or like the CodeIgniter routes, there we go:

namespace Core;

use Core\Route;
use Helpers\Request;
use Helpers\Url;

/**
 * ClassicRouter class will load requested controller / closure based on url.
 */
class ClassicRouter extends \Core\Router
{
    // Constructor
    public function __construct()
    {
        parent::__construct();
    }

   /**
     * Ability to call controllers in their module/directory/controller/method/param way.
     */
    public function autoDispatch($uri)
    {
        // NOTE: This Auto-Dispatch routing use the styles:
        //
        // <DIR><directory><controller><method><params>
        // <DIR><module><directory><controller><method><params>

        // Split the URI in the path components.
        $parts = explode('/', $uri);

        // Loop through URI parts, checking for the Controller file including its path.
        $controller = '';

        if(! empty($parts)) {
            // Classify, to permit: '<DIR>/file_manager/admin/' -> '<SMVC>/Modules/FileManager/Admin/
            $controller = str_replace(array('-', '_'), '', ucwords(array_shift($parts), '-_'));
        }

        // Verify if the first URI part match a Module.
        $testPath = SMVC.'Modules/'.$controller;

        if(! empty($controller) && is_dir($testPath)) {
            // Walking in a Module path.
            $moduleName = $controller;
            $basePath   = 'Modules/'.$controller.'/Controllers/';

            // Go further only if have other URI Parts, to permit URL mappings like:
            // '<DIR>/clients' -> '<SMVC>/Modules/Clients/Controllers/Clients.php'
            if(! empty($parts)) {
                $controller = str_replace(array('-', '_'), '', ucwords(array_shift($parts), '-_'));
            }
        }
        else {
            $moduleName = '';
            $basePath   = 'Controllers/';
        }

        // Check for a valid Controller even in sub-directories.
        $directory = '';

        while (! empty($parts)) {
            $testPath = SMVC.$basePath.$directory.$controller;

            if (! is_readable($testPath .'.php') && is_dir($testPath)) {
                $directory .= $controller .'/';
                $controller = str_replace(array('-', '_'), '', ucwords(array_shift($parts), '-_'));

                continue;
            }

            break;
        }

        // Get the normalized Controller
        $defaultOne = !empty($moduleName) ? $moduleName : DEFAULT_CONTROLLER;
        $controller = !empty($controller) ? $controller : $defaultOne;

        // Get the normalized Method
        $method = !empty($parts) ? array_shift($parts) : DEFAULT_METHOD;

        // Get the Controller's className.
        $controller = str_replace('/', '\\', 'App/'.$basePath.$directory.$controller);

        // Controller's Methods starting with '_' are not allowed also to be called on Router.
        if(($method[0] === '_') || !class_exists($controller)) {
            return false;
        }

        // Initialize the Controller
        $controller = new $controller();

        // Check for a valid public Controller's Method.
        if(! in_array(strtolower($method), array_map('strtolower', get_class_methods($controller)))) {
            return false;
        }

        // Execute the current Controller's Method with the given arguments.
        call_user_func_array(array($controller, $method), !empty($parts) ? $parts : array());

        return true;
    }

    public function dispatch()
    {
        // Detect the URI and the HTTP Method.
        $uri = Url::detectUri();

        $method = Request::getMethod();

        foreach ($this->routes as $route) {
            if ($route->match($uri, $method)) {
                // Found a valid Route; invoke the autoDispatch and go out.
                $callback = $route->callback();

                if(! is_object($callback)) {
                    $autoUri = preg_replace('#^' .$route->regex() .'$#', $callback, $uri);

                    $this->autoDispatch($autoUri);
                }
                else {
                    $this->invokeObject($callback, $route->params());
                }

                return true;
            }
        }

        // No valid Route found; invoke the Error Callback with the current URI as parameter.
        $params = array(
            htmlspecialchars($uri, ENT_COMPAT, 'ISO-8859-1', true)
        );

        $this->invokeObject($this->callback(), $params);

        return false;
    }

}

To activate the ClassicRouter we need to setup:

/**
 * Set the Application Router.
 */
define('APPROUTER', '\Core\ClassicRouter');

To note that new ClassicRouter have much greater performance compared with the orthodox autoDispatcher method of the actual \Core\Router, supporting patterns, just like the base \Core\Router, and supporting also, very important, the sub-directories on App and Modules. Then, you can happily use, for example, for your controllers paths, the styles:

/admin/blog              -> /app/Controllers/Admin/Blog.php                @index
/user                    -> /app/Modules/User/Controllers/User.php         @index

/admin/users             -> as route pattern to following...
/user/admin/users/list   -> /app/Modules/User/Controllers/Admin/Users.php  @list

You can even do:

Router::any('/admin/(:any)/(:all)', '$1/admin/$2');

OR

Router::any('/admin/(:any)/(:all)', '$1/admin/$1/$2');

which will map the following Routes like as:

/admin/page/add     -> /page/admin/add     -> /app/Modules/Page/Controllers/Admin.php   @add
/admin/blog/view/15 -> /blog/admin/view/15 -> /app/Modules/Blog/Controllers/Admin.php   @view
/admin/users/edit/1 -> /users/admin/edit/1 -> /app/Modules/Users/Controllers/Admin.php  @edit

OR

/admin/page/add     -> /page/admin/page/add      -> /app/Modules/Page/Controllers/Admin/Page.php    @view
/admin/blog/view/15 -> /blog/admin/blog/view/15  -> /app/Modules/Blog/Controllers/Admin/Blog.php    @view
/admin/users/edit/1 -> /users/admin/users/edit/1 -> /app/Modules/Users/Controllers/Admin/Users.php  @edit

And, yeah, if the SVMC 3.0 is driven by this enhanced Router, not only you have, native, a better Routing, but us we can easily extend the default Router to support our Asset Files Serving style and other things, without to completely replace the main thing.

Finally, I will ask if there is interest for a simple but very powerful \Core\Language class replacement, which support the PHP's 5.5.x native INTL (based on ICU), with support for single language file per App and Module or Template, loading multiple language domains, etc. It needs the Config class mentioned in my previous (long) post, some functions to be defined in index.php or a configuration file, but the result is somewhat similar with what can be seen in CakePHP 3.0

Example of usage:

Session::set('message', array('type' => 'success', 'text' => __d('user', 'User <b>{0}</b> was Added', $userinfo['username'])));

Notice the function called __d(), that's command read the language file from the domain (of Module) User and translate the message with support for parameters.

BTW, our Language class have as companion a (Linux) script able to scan the PHP files from framework, to extract the translatable strings and to update the language files on App and Modules.

All the best!

jimgwhit commented 8 years ago

@LuckyCyborg does the router you show handle a regular querystring like

Site/dogs?page=4&search=b

Or will I be forced into

Site/dogs/4/b

I don't like friendly url's I prefer querystring.
By the way Laravel you can use querystring.
And above you said. maybe can accommodate our requirements, without us to rewrite its Core Components.
Just curious if 50 different people had 50 different sets of requirements then what? Hey just a question there. Where is your github code located, I would love to try it out?

BryanYeh commented 8 years ago

I would like to comment but got nothing to say, this is going way too far ahead of me. (Not that I tried v3 yet) There should be an Authentication/Authorization class?

tomvlk commented 8 years ago

@LuckyCyborg I really like your routing methods, more object oriented. But will it affect the performance? (Not always important btw, maintainability is more important in my opinion. but just curious).