File Coverage

blib/lib/PlackX/Framework.pm
Criterion Covered Total %
statement 81 81 100.0
branch 11 16 68.7
condition 12 17 70.5
subroutine 21 21 100.0
pod 0 6 0.0
total 125 141 88.6


line stmt bran cond sub pod time code
1             # strict (5.12), warnings (5.35), signatures (5.36)
2 18     18   1728786 use v5.36;
  18         56  
3              
4             package PlackX::Framework 0.26 {
5 18     17   3479 use PXF::Util ();
  17         77  
  17         315  
6 17     13   119 use List::Util qw(any);
  13         40  
  13         5604  
7              
8             our @plugins = ();
9 32     32 0 147 sub required_modules { qw(Handler Request Response Router Router::Engine) }
10 18     18 0 103 sub optional_modules { qw(Config Template URIx), @plugins }
11              
12             # Export ->app, load parent classes and load or create subclasses
13 18     18   22676 sub import (@options) {
  18         68  
  18         54  
14 18         72 my %required = map { $_ => 1 } required_modules(); # not memoized to save ram
  74         237  
15 18     18   148 my $want_all = any { $_ =~ m/^[:+]all$/ } @options;
  22         76  
16 18   66 39   222 my $want_mod = ($want_all or sub { any { $_ =~ m/^[:+]{0,2}$_[0]$/i } @options });
  39         291  
  48         2083  
17 14         47 my $caller = caller(0);
18 14         53 export_app_sub($caller);
19              
20             # Load or create required modules, attempt to load optional ones
21             # Distinguish between modules not existing and modules with errors
22 14         42 foreach my $module (required_modules(), optional_modules()) {
23             eval 'require PlackX::Framework::'.$module
24 112 100 50     5300 or die $@ if $required{$module};
25 112 50       8788 eval 'require '.$caller.'::'.$module or do {
26 112 50       630 die $@ if PXF::Util::is_module_broken($caller.'::'.$module);
27             generate_subclass($caller.'::'.$module, 'PlackX::Framework::'.$module)
28 112 100 100     787 if $required{$module} or $want_all or $want_mod->($module);
      100        
29             };
30 112 100       779 export_app_namespace_sub($caller, $module)
31             if PXF::Util::is_module_loaded($caller.'::'.$module);
32             }
33             }
34              
35             # Export app() sub to the app's main package
36 14     14 0 25 sub export_app_sub ($to_package) {
  14         29  
  14         59  
37 14     10   113 my $code = sub ($class, @opts) { ($class.'::Handler')->build_app(@opts) };
  10         84  
  10         2560  
  10         22  
  10         17  
  10         16  
38 13     13   103 no strict 'refs';
  13         45  
  13         920  
39 14         33 *{$to_package.'::app'} = *{$to_package.'::to_app'} = $code;
  14         92  
  14         101  
40             }
41              
42             # Export app_namespace() to App::Request, App::Response, etc.
43 76     76 0 132 sub export_app_namespace_sub ($namespace, $module) {
  76         134  
  76         120  
  76         104  
44 13     13   72 no strict 'refs';
  13         71  
  13         3257  
45 76         5756 my $exists = eval $namespace.'::'.$module.'::app_namespace()';
46 76 50 33     484 die "app_namespace(): expected $namespace, got $exists" if $exists and $exists ne $namespace;
47 76 50   164   533 *{$namespace.'::'.$module.'::app_namespace'} = sub { $namespace } unless $exists;
  76         681  
  164         10403  
48             }
49              
50             # Helper to create a subclass and mark as loaded
51 76     76 0 133 sub generate_subclass ($new_class, $parent_class) {
  76         127  
  76         170  
  76         119  
52 76 50   7   7278 eval "package $new_class; use parent '$parent_class'; 1" or die "Cannot create class: $@";
  7     7   1960  
  7     7   1441  
  7     7   53  
  7     7   83  
  7         17  
  7         72  
  7         47  
  7         17  
  7         44  
  7         64  
  7         85  
  7         55  
  7         49  
  7         15  
  7         51  
53 76         331 PXF::Util::mark_module_loaded($new_class);
54             }
55              
56             # Keep name to 16B. Memoize so we don't have compute md5 each time.
57 46     46 0 76 sub flash_cookie_name ($class) {
  46         99  
  46         64  
58 46   66     65 state %names; $names{$class} ||= 'flash'. PXF::Util::md5_ushort($class, 11);
  46         225  
59             }
60             }
61              
62             1;
63              
64             =pod
65              
66             =head1 NAME
67              
68             PlackX::Framework - A thin framework for PSGI/Plack web apps.
69              
70              
71             =head1 SYNOPSIS
72              
73             This is a small framework for PSGI web apps, based on Plack. A simple
74             PlackX::Framework application could be all in one .psgi file:
75              
76             # app.psgi
77             package MyProject {
78             use PlackX::Framework; # loads and sets up the framework and subclasses
79             use MyProject::Router; # exports router DSL
80             route '/' => sub ($request, $response) {
81             $response->body('Hello, ', $request->param('name'));
82             return $response;
83             };
84             }
85             MyProject->app;
86              
87             A larger application would be typically laid out with separate modules in
88             separate files, for example in MyProject::Controller::* modules. Each should
89             use MyProject::Router if the DSL-style routing is desired.
90              
91             This software is considered to be in an experimental, "alpha" stage.
92              
93              
94             =head1 DESCRIPTION
95              
96             =head2 Overview and Required Components
97              
98             PlackX::Framework consists of the required modules:
99              
100             =over 4
101              
102             =item PlackX::Framework
103              
104             =item PlackX::Framework::Handler
105              
106             =item PlackX::Framework::Request
107              
108             =item PlackX::Framework::Response
109              
110             =item PlackX::Framework::Router
111              
112             =item PlackX::Framework::Router::Engine
113              
114             =back
115              
116             And the following optional modules:
117              
118             =over 4
119              
120             =item PlackX::Framework::Config
121              
122             =item PlackX::Framework::Template
123              
124             =item PlackX::Framework::URIx
125              
126             =back
127              
128             The statement "use PlackX::Framework" will automatically find and load all of
129             the required modules. Then it will look for subclasses of the modules listed
130             above that exist in your namespace and load them, or create empty subclasses
131             for any required modules that do not exist. The following example
132              
133             package MyProject {
134             use PlackX::Framework;
135             # ...app logic here...
136             }
137              
138             will attempt to load MyProject::Handler, MyProject::Request,
139             MyProject::Response and so on, or create them (in memory, not on disk) if
140             they do not exist.
141              
142             You only use, not inherit from, PlackX::Framework. However, your
143             ::Handler, ::Request, ::Response, etc. classes should inherit from
144             PlackX::Framework::Handler, ::Request, ::Response, and so on.
145              
146              
147             =head2 Optional Components
148              
149             The Config, Template, URIx modules are included in the distribution, but
150             loading them is optional to save memory and compile time when not needed.
151             Just as with the required modules, you can subclass them yourself, or you can
152             have them automatically generated.
153              
154             To set up all optional modules, import with the :all (or +all) tag.
155              
156             # The following are equivalent
157             use PlackX::Framework qw(:all);
158             use PlackX::Framework qw(+all);
159              
160             Note that 'use Module -option' syntax is not supported, because it can be mis-
161             read by human readers as "minus option" which might give the impression that
162             the named option is being turned off.
163              
164             If you want to pick certain optional modules, you can specify those
165             individually with the name of the module, optionally preceded by a single
166             double colon (: or ::) or a plus sign. You may also use lower case.
167              
168             # All of the below are equivalent
169             use PlackX::Framework qw(Config Template);
170             use PlackX::Framework qw(:Config :Template);
171             use PlackX::Framework qw(:config :template);
172             use PlackX::Framework qw(::Config ::Template);
173             use PlackX::Framework qw(+Config +Template);
174              
175             Third party developers can make additional optional components available, by
176             pushing to the @PlackX::Framework::plugins array. These can then be loaded by
177             an application same way as the bundled optional modules.
178              
179              
180             =head2 The Pieces and How They Work Together
181              
182             =head3 PlackX::Framework
183              
184             PlackX::Framework is basically a management module that is responsible for
185             loading required and optional components. It will automatically subclass
186             required, and desired optional classes for you if you have not done so already.
187             It exports one symbol, app(), to the calling package; it also exports an
188             app_namespace() sub to your app's subclasses, which returns the name of the
189             root class.
190              
191             # Example app
192             package MyApp {
193             # The following statement will load, or automatically create,
194             # MyApp::Handler, MyApp::Request, MyApp::Response, MyApp::Router, etc.
195             # It will create a MyApp::app() function, and an app_namespace() function
196             # in each respective subclassed module if one does not already exist.
197             use PlackX::Framework qw(:all);
198             }
199              
200              
201              
202             =head3 PlackX::Framework::Handler
203              
204             PlackX::Framework::Handler is the package responsible for request processing.
205             You would not normally have to subclass this module manually unless you would
206             like to customize its behavior. It will prepare request and response objects,
207             a stash, and if set up, templating.
208              
209              
210             =head3 PlackX::Framework::Request
211              
212             =head3 PlackX::Framework::Response
213              
214             The PlackX::Framework::Request and PlackX::Framework::Response modules are
215             subclasses of Plack::Request and Plack::Response sprinkled with additional
216             features, described below.
217              
218             =over 4
219              
220             =item stash()
221              
222             Both feature a shared "stash" which is a hashref in which you can store any
223             data you would like. The "stash" is not a user session but a way to
224             temporarily store information during a request/response cycle. It is
225             re-initialized for each cycle.
226              
227             =item flash()
228              
229             They also feature a "flash" cookie which you can use to store information on
230             the user end for one cycle. It is automatically cleared in the following
231             cycle. For example...
232              
233             $response->flash('Goodbye!'); # Store message in a cookie
234              
235             On the next request:
236              
237             $request->flash; # Returns 'Goodbye!'.
238              
239             During the response phase, the flash cookie is cleared, unless you set another
240             one.
241              
242             =back
243              
244             =head3 PlackX::Framework::Router
245              
246             This module exports the route, route_base, global_filter, and filter functions
247             to give you a minimalistic web app controller DSL. You can import this into
248             your main app package, as shown in the introduction, or separate packages.
249              
250             # Set up the app
251             package MyApp {
252             use PlackX::Framework;
253             }
254              
255             # Note: The name of your controller module doesn't matter, but it must
256             # import from your router subclass, e.g., MyApp::Router, not directly from
257             # PlackX::Framework::Router!
258             package MyApp::Controller {
259             use MyApp::Router;
260              
261             base '/app';
262              
263             global_filter before => sub {
264             # I will be executed for ANY route ANYWHERE in MyApp!
265             ...
266             };
267              
268             filter before => sub {
269             # I will only be executed for the routes listed below in this package.
270             ...
271             };
272              
273             route '/home' => sub {
274             ...
275             };
276              
277             route { post => '/login' } => sub {
278             ...
279             };
280             }
281              
282              
283             =head3 PlackX::Framework::Router::Engine
284              
285             The PlackX::Framework::Router::Engine is a subclass of Router::Boom with some
286             extra convenience methods. Normally, you would not have to use this module
287             directly. It is used by PlackX::Framework::Router internally.
288              
289              
290             =head3 PlackX::Framework::Config
291              
292             This module is provided primarily for convenience. Currently not used by PXF
293             directly except you may optionally store template system configuration there.
294              
295              
296             =head3 PlackX::Framework::Template
297              
298             The PlackX::Framework::Template module can automatically load and set up
299             Template Toolkit, offering several convenience methods. If you desire to use
300             a different templating system from TT, you may override as many methods as
301             necessary in your subclass. A new instance of this class is generated for
302             each request by the app() method of PlackX::Framework::Handler.
303              
304              
305             =head3 PlackX::Framework::URIx
306              
307             The PlackX::Framework::URIx is a URI processing module with extended features,
308             and it is a subclass of URI::Fast. It is made available to your
309             your request objects through $request->urix (the x is to not confuse it
310             with the Plack::Request->uri() method). If you have not enabled the URIx
311             feature in your application, with the :URIx or :all tag, the request->urix
312             method will cause an error.
313              
314              
315             =head2 Why Another Framework?
316              
317             Plack comes with several modules that make it possible to create a bare-bones
318             web app, but as described in the documentation for Plack::Request, that is a
319             very low-level way to do it. A framework is recommended. This package
320             provides a minimalistic framework which takes Plack::Request, Plack::Response,
321             and several other modules and ties them together.
322              
323             The end result is a simple framework that is higher level than using the raw
324             Plack building blocks, although it does not have as many features as other
325             frameworks. Here are some advantages:
326              
327             =over 4
328              
329             =item Load Time
330              
331             A basic PlackX::Framework "Hello World" application loads 75% faster
332             than a Dancer2 application and 70% faster than a Mojolicious::Lite app.
333             (The author has not benchmarked request/response times.)
334              
335             =item Memory
336              
337             A basic PlackX::Framework "Hello World" application uses approximately
338             one-third the memory of either Dancer2 or Mojolicious::Lite (~10MB compared
339             to ~30MB for each of the other two).
340              
341             =item Dependencies
342              
343             PlackX::Framework has few non-core dependencies (it has more than
344             Mojolicious, which has zero, but fewer than Dancer2, which has a lot.)
345              
346             =item Magic
347              
348             PlackX::Framework has some magic, but not too much. It can be easily
349             overriden with subclassing. You can use the bundled router engine
350             or supply your own. You can use Template Toolkit automatically or use
351             a different template engine.
352              
353             =back
354              
355             The author makes no claims that this framework is better than any other
356             framework except for the few trivial metrics described above. It has been
357             published in the spirit of TIMTOWDI.
358              
359             =head2 Why Now?
360              
361             The project was started in 2016, and is used in production by its author.
362             It seemed well past time to publish it to CPAN (better late than never?).
363              
364              
365             =head2 Object Orientation and Magic
366              
367             PlackX::Framework has an object-oriented design philosophy that uses both
368             inheritance and composition to implement its features. Symbols exported are
369             limited to avoid polluting your namespace, however, a lot of the "magic" is
370             implemented with the import() method, so be careful about using empty
371             parenthesis in your use statements, as this will prevent the import() method
372             from being called and may break things.
373              
374             Also be careful about whether you should use a module or subclass it.
375             Generally, modifying the behavior of the framework itself will involve
376             manual subclassing, while using the framework as-is will not.
377              
378              
379             =head2 Configuration
380              
381             =head3 app_base
382              
383             =head3 uri_prefix
384              
385             In your application's root namespace, you can set the base URL for requests
386             by defining an app_base subroutine; uri_prefix can be used as a synonym.
387              
388             package MyApp {
389             use PlackX::Framework;
390             sub app_base { '/app' } # or uri_prefix
391             }
392              
393             Internally, this uses Plack::App::URLMap to cleave the base from the path_info.
394             This feature will not play well if you mount your app to a particular uri path
395             using Plack::Builder. Use one or the other, not both. If you would like to give
396             your app flexibility for different environments, you could do something like
397             the following:
398              
399             # Main app package
400             package MyApp {
401             use PlackX::Framework;
402             sub app_base { $ENV{'myapp_base'} }
403             }
404              
405             # one app .psgi file which uses Builder
406             use Plack::Builder;
407             $ENV{'myapp_base'} = '';
408             builder {
409             mount '/myapp' => MyApp->app;
410             ...
411             };
412              
413             # another app .psgi file, perhaps on a different server, not using Builder
414             $ENV{'myapp_base'} = '/myapp';
415             MyApp->app;
416              
417              
418             =head2 Routes, Requests, and Request Filtering
419              
420             See PlackX::Framework::Router for detailed documentation on request routing and
421             filtering.
422              
423              
424             =head2 Templating
425              
426             No Templating system is loaded by default, but PlackX::Framework can
427             automatically load and set up Template Toolkit if you:
428              
429             use MyProject::Template;
430              
431             (assuming MyProject has imported from PlackX::Framework).
432              
433             Note that this feature relies on the import() method of your app's
434             PlackX::Framework::Template subclass being called (this subclass is also
435             created automatically if you do not have a MyApp/Template.pm file).
436             Therefore, the following will not load Template Toolkit:
437              
438             use MyApp::Template (); # Template Toolkit is not loaded
439             require MyApp::Template; # Template Toolkit is not loaded
440              
441             If you want to supply Template Toolkit with configuration options, you can
442             add them like this
443              
444             use MyApp::Template (INCLUDE_PATH => 'template');
445              
446             If you want to use your own templating system, you can create a MyApp::Template
447             module that subclasses PlackX::Framework::Template, then override necessary
448             methods; however, a simpler way is available if your templating system as a TT
449             compatible process method, like this:
450              
451             use MyApp::Template qw(:manual);
452             MyApp::Template->set_engine(My::Template::System->new(%options));
453              
454              
455             =head2 Model Layer
456              
457             This framework is databse/ORM agnostic, you are free to choose your own or use
458             plain DBI/SQL.
459              
460              
461             =head1 EXPORT
462              
463             This module will export the "app" method, which returns the code reference of
464             your app in accordance to the PSGI specification. (This is actually a shortcut
465             to [ProjectName]::Handler->build_app.)
466              
467              
468             =head1 DEPENDENCIES
469              
470             =head2 Required
471              
472             =over 4
473              
474             =item perl 5.36 or greater
475              
476             =item Plack
477              
478             =item Router::Boom
479              
480             =item URI::Fast
481              
482             =back
483              
484              
485             =head2 Optional
486              
487             =over 4
488              
489             =item Config::Any
490              
491             =item Template
492              
493             =back
494              
495             =head1 SEE ALSO
496              
497             =over 4
498              
499             =item PSGI
500              
501             =item Plack
502              
503             =item Plack::Request
504              
505             =item Plack::Response
506              
507             =item Router::Boom
508              
509             =back
510              
511              
512             =head1 AUTHOR
513              
514             Dondi Michael Stroma, Edstroma@gmail.comE
515              
516              
517             =head1 COPYRIGHT AND LICENSE
518              
519             Copyright (C) 2016-2026 by Dondi Michael Stroma
520              
521              
522             =cut