Skip to content

Repository files navigation

PHP web framework optimized for building Business Applications

Created as simplified and lightweight alternative to other PHP frameworks

image

Features

  • simple and straightforward in development and maintenance
  • MVC-like
    • code, data, templates are split
    • code consists of: controllers, models, framework core and optional 3rd party libs
    • uses ParsePage template engine
    • data stored by default in MySQL database using db.php
  • RESTful with some practical enhancements
  • integrated auth - simple flat access levels auth
  • UI based on Bootstrap 5 with minimal custom CSS and themes support - it's easy to customzie or apply your own theme
  • use of well-known 3rd party libraries: jQuery, jQuery Form, jGrowl, markdown libs, etc...

Documentation

Development/Deployment

  1. point your web server document root to www/
  2. run composer install from php/
  3. run php bin/libman.php from the repository root to install browser libraries
  4. edit php/configs/site.php (or php/configs/localhost.php for development)
  5. create database from /db/fwdatabase.sql, /db/lookups.sql and others (if needed)
  6. open site in your browser and login with credentials as defined in fwdatabase.sql
  7. review log in /logs/osafw.log

Directory structure

/db - initial fwdatabase.sql script and update sql scripts
/logs/osafw.log - application log (ensure to enable write rights to /logs dir for webserver)
/www - application public root folder
/assets - your web frontend assets
/css
/fonts
/img
/js
/favicon.ico - change to your favicon!
/robots.txt - default robots.txt (empty)
/php - PHP code outside the public web root
/controllers - your controllers
/fw - framework core libs
/models - your models
/tools - non-public developer/admin tools
/vendor - composer libs
/configs - settings for db connection, mail, logging...
/template - ParsePage templates outside the public web root
/upload - non-public attachment storage served through framework routes

REST mappings

Controllers automatically directly mapped to URLs, so developer doesn't need to write routing rules:

  • GET /Controller - list view IndexAction()
  • GET /Controller/ID - one record view ShowAction()
  • GET /Controller/new - one record new form ShowFormAction()
  • GET /Controller/ID/edit - one record edit form ShowFormAction()
  • GET /Controller/ID/delete - one record delete confirmation form ShowDeleteAction()
  • POST /Controller - insert new record SaveAction()
  • PUT /Controller - update multiple records SaveMultiAction()
  • POST/PUT /Controller/ID - update record SaveAction()
  • DELETE /Controller/ID - delete record DeleteAction()
  • GET/POST /Controller/(Something)[/ID] - call for arbitrary action from the controller SomethingAction()
  • GET/POST /Controller/Something[/ID] - call for arbitrary action from the controller SomethingAction(), in this case action name should be less than 32 characters

For example GET /Products will call ProductsController.IndexAction() And this will cause rendering templates from template/products/index

ID can be numeric or 32-char string like UUID (without dashes) For example GET /Products/123 will call ProductsController.ShowAction(123)

Request Flow

highlighted as bold is where you could place your code.

  • FW.run()
    • FwHooks.initRequest() - place code here which need to be run on request start
  • fw.dispatch() - performs REST urls matching and calls controller/action, if no controller found calls HomeController.NotFoundAction(), if no requested action found in controller - calls controller action defined in contoller's route_default_action (either "index" or "show")
    • fw._auth() - check if user can access requested controller/action, also performs basic CSRF validation
    • fw.call_controller()
      • SomeController.init() - place code here which need to be run every time request comes to this controller
      • SomeController.SomeAction() - your code for particular action
        • SomeModel.someMethod() - controllers may call model's methods, place most of your business logic in models
  • fw.Finalize()

Examples:

  • GET /Admin/Users

    • FwHooks.initRequest()
    • AdminUsers.init()
    • AdminUsers.IndexAction()
    • then ParsePage parses templates from /template/admin/users/index/
  • GET /Admin/Users/123/edit

    • FwHooks.initRequest()
    • AdminUsers.init()
    • AdminUsers.ShowFormAction(123)
      • Users.one(123)
    • then ParsePage parses templates from /template/admin/users/showform/
  • POST /Admin/Users/123

    • FwHooks.initRequest()
    • AdminUsers.init()
    • AdminUsers.SaveAction(123)
      • Users.update(123)
    • fw.redirect("/Admin/Users/123/edit") //redirect back to edit screen after db updated
  • GET /Admin/Users/(Custom)/123?param1=1&param2=ABC - controller's custom action (non-standard REST)

    • FwHooks.initRequest()
    • AdminUsers.init()
    • AdminUsers.CustomAction(123) - here you can get params using reqi("param1") -> 1 and reqs("params") -> "ABC"
    • then ParsePage parses templates from /template/admin/users/custom/ unless you redirect somewhere else
  • POST /Admin/Users/(Custom)/123 with posted params param1=1 and param2=ABC

    • FwHooks.initRequest()
    • AdminUsers.init()
    • AdminUsers.CustomAction(123) - here you can still get params using reqi("param1") -> 1 and reqs("params") -> "ABC"
    • then ParsePage parses templates from /template/admin/users/custom/ unless you redirect somewhere else

Flow in IndexAction

Frequently asked details about flow for the IndexAction() (in controllers inherited from FwAdminController and FwDynamicController):

  1. initFilter() - initializes this.list_filter from query string filter params &f[xxx]=..., note, filters remembered in session
  2. setListSorting() - initializes this.list_orderby based on list_filter("sortby") and list_filter("sortdir"), also uses this.list_sortdef and this.list_sortmap which can be set in controller's init() or in config.json
  3. setListSearch() - initializes this.list_where based on list_filter("s") and this.search_fields
  4. setListSearchStatus() - add to this.list_where filtering by status field if such field defined in the controller's model
  5. getListRows() - query database and save rows to this.list_rows (only current page based on this.list_filter("pagenum") and this.list_filter("pagesize")). Also sets this.list_count to total rows matched by filters and this.list_pager for pagination if there are more than one page. Uses this.list_view, this.list_where, this.list_orderby

You could either override these particular methods or whole IndexAction() in your specific controller.

The following controller fields used above can be defined in controller's init() or in config.json:

  • this.list_sortdef - default list sorting in format: "sort_name[ asc|desc]"
  • this.list_sortmap - mapping for sort names (from list_filter["sortby"]) to actual db fields, Hashtable sort_name => db_field_name
  • this.search_fields - search fields, space-separated
  • this.list_view - table/view to use in getListRows(), if empty model's table_name used

fw.config

Application configuration available via fw.config->[SettingName]. Most of the global settings defined in config.*.php. But there are several caclulated settings:

For HS256 JWT authentication, define JWT_SECRET in the site or local config as a cryptographically random secret of at least 32 bytes. firebase/php-jwt 7 rejects shorter secrets.

SettingNameDescriptionExample
hostnameset from server variable HTTP_HOSTosalabs.com
ROOT_DOMAINprotocol+hostnamehttps://osalabs.com
ROOT_URLpart of the url if Application installed under sub-url/suburl if App installed under osalabs.com/suburl
site_rootphysical application path to the root of public directorywww
templatephysical path to the root of templates directorytemplate
logphysical path to application log filelogs/osafw.log
tmpphysical path to the system tmp directorysystem temp directory

config.json

In FwDynamicController controller behaviour defined by /template/CONTROLLER/config.json. Sample file can be fount at /template/admin/demosdynamic/config.json This config file allows to define/override several properties of the FwController (for example: as model, save_fields, search_fields, list_view,...) as well as define configuration of Show (show_fields) and ShowForm ( showform_fields) screens. Note is_dynamic_show and is_dynamic_showform should be set to true accordingly. There are samples for the one show_fields or showform_fields element:

//minimal setup to display the field value
{
"type": "plaintext",
"field": "iname",
"label": "Title"
},

Renders:

<divclass="form-row"><labelclass="col-form-label">Title</label><divclass="col"><pclass="form-control-plaintext">FIELD_VALUE</p></div></div>
//more complex - displays dropdown with values from lookup model
{
"type": "select",
"field": "demo_dicts_id",
"label": "DemoDicts",
"lookup_model": "DemoDicts",
"is_option0": true,
"class_contents": "col-md-3",
"class_control": "on-refresh"
},

Renders:

<divclass="form-row"><labelclass="col-form-label">DemoDicts</label><divclass="col-md-3"><selectid="demo_dicts_id" name="item[demo_dicts_id]" class="form-control on-refresh"><optionvalue="0">- select -</option>
... select options from lookup here...
</select></div></div>
Field nameDescriptionExample
typerequired, Element type, see values in table belowselect - renders as <select> html
fieldField name from database.table or arbitrary name for non-db blockdemo_dicts_id - in case of select id value won't be displayed, but used to select active list element
labelLabel textDemo Dictionary
lookup_modelModel name where to read lookup valuesDemoDicts
is_option0only for "select" type, if true - includes <option value="0">option0_title</option>false(default),true
is_option_emptyonly for "select" type, if true - includes <option value="">option0_title</option>false(default),true
option0_titleonly for "select" type for is_option0 or is_option_empty option title"- select -"(default)
requiredmake field required (both client and server-side validation), for showform_fields onlyfalse(default),true
maxlengthset input's maxlength attribute, for showform_fields only10
maxset input type="number" max attribute, for showform_fields only999
minset input type="number" min attribute, for showform_fields only0
stepset input type="number" step attribute, for showform_fields only0.1
placeholderset input's maxlength attribute, for showform_fields only"Enter value here"
autocomplete_urltype="autocomplete". Input will get data from autocomplete_url?q=%QUERY where %QUERY will be replaced with input value, for showform_fields only/Admin/SomeLookup/(Autocompete)
is_inlinetype radio or yesno. If true - place all options in one line, for showform_fields onlytrue(default),false
rowsset textarea rows attribute, for showform_fields only5
classClass(es) added to the wrapping div.form-rowmb-2 - add bottom margin under the control block
attrsArbitrary html attributes for the wrapping div.form-rowdata-something="123"
class_labelClass(es) added to the label.col-form-labelcol-md-3(default) - set label width
class_contentsClass(es) added to the div that wraps input controlcol(default) - set control width
class_controlClass(es) added to the input control to change appearance/behaviour"on-refresh" - forms refreshes(re-submits) when input changed
attrs_controlArbitrary html attributes for the input controldata-something="123"
help_textHelp text displayed as muted text under control block"Minimum 8 letters and digits required"
admin_urlFor type="plaintext_link", controller url, final URL will be: "<~admin_url>/<~lookup_id>"/Admin/SomeController
lookup_idto use with admin_url, if link to specific ID required123
att_categoryFor type="att_edit", att category new upload will be related to"general"(default)
validateSimple validation codes: exists, isemail, isphone, isdate, isfloat"exists isemail" - input value validated if such value already exists, validate if value is an email
type values
TypeDescription
available for both show_fields and showform_fields
plaintextPlain text
plaintext_linkPlain text with a link to "admin_url"
markdownMarkdown text (server-side rendered)
noescapeValue without htmlescape
floatValue formatted with 2 decimal digits
checkboxRead-only checkbox (checked if value equal to true value)
dateDate in default format - M/d/yyyy
date_longDate in logn forma - M/d/yyyy hh:mm:ss
multiMulti-selection list with checkboxes (read-only)
attBlock for displaying one attachment/file
att_linksBlock for displaying multiple attachments/files
addedAdded on date/user block
updatedUpdated on date/user block
available only showform_fields
group_idID with Submit/Cancel buttons block
group_id_addnewID with Submit/Submit and Add New/Cancel buttons block
selectselect with options html block
inputinput type="text" html block
textareatextaread html block
emailinput type="email" html block
numberinput type="number" html block
autocompleteinput type="text" with autocomplete using "autocomplete_url"
multicbMulti-selection list with checkboxes
radioradio options block
yesnoradio options block with Yes(1)/No(2) only
cbsingle checkbox block
date_popupdate selection input with popup calendar block
att_editBlock for selection/upload one attachment/file
att_links_editBlock for selection/upload multiple attachments/files

How to Debug

Main and recommended approach - use logger() function, which is globally available. Examples: logger("some string to log", var_to_dump), logger("WARN", "warning message") All logged messages and var content (complex objects will be dumped wit structure when possible) written on debug console as well as to log file (default /logs/osafw.log) You could configure log level in your config.*.php - search "LOG_LEVEL"

Another debug functions that might be helpful are:

  1. rw($var) this function will work like var_dump and just dump variable structure and data to browser (with some formatting)
  2. rwe($var) same as above, but immediately die to stop script

Best Practices / Recommendations

  • naming conventions:
    • table name: user_lists (lowercase, underscore delimiters is optional)
    • model name: UserLists (UpperCamelCase)
    • controller name: UserListsController or AdminUserListsController (UpperCamelCase with "Controller" suffix)
    • template path: /template/userlists
  • keep all paths without trailing slash, use beginning slash where necessary
  • db updates:
    • first, make changes in /db/fwdatabase.sql - this file is used to create db from scratch
    • then create a file /db/updates/updYYYY-MM-DD[-123].sql with all the CREATE, ALTER, UPDATE... - this will allow to apply just this update to existing database instances
  • use fw.route_redirect() if you got request to one Controller.Action, but need to continue processing in another Controller.Action
    • for example, if for a logged user you need to show detailed data and always skip list view - in the IndexAction() just use fw.routeRedirect("ShowForm")
  • uploads
    • save attachment files under upload/ outside the public web root
    • access default file/table attachments through /Att/<id> URLs
    • set PUBLIC_UPLOAD_URL only when you intentionally expose a separate upload base through the web server
    • or use S3 model and upload to the cloud
  • put all validation code into controller's Validate(). See usage example in AdminDemosController
  • use logger() and review /logs/osafw.log if you stuck
    • make sure you have "LOG_LEVEL" set to "DEBUG" in your config.*.php

How to quickly create a Report

  • all reports accessed via AdminReportsController
    • IndexAction - shows a list of all available reports (basically renders static html template with a link to specific reports)
    • ShowAction - based on passed report code calls related Report model
  • base report model is FwReports, major methods (you may override in the specific report):
    • getReportFilters() - set data for the report filters
    • getReportData() - returns report data, usually based on some sql query (see Sample report)
  • ReportSample model (in php/models/Reports folder) is a sample report implementation, that can be used as a template to build custom reports
  • basic steps to create a new report:
    • copy php/models/Reports/Sample.php to php/models/Reports/Cool.php (to create Cool report)
    • edit Cool.php and rename "Sample" to "Cool"
    • modify getReportFilters() to match your report filters
    • modify getReportData() to edit sql query and related post-processing
    • copy templates folder template/reports/sample to template/reports/cool
    • edit templates:
      • title.html - report title
      • list_filter.html - for filters
      • report_html.html - for report table/layout/appearance
    • add link to a new report to template/reports/index/main.html

About

Business Applications Web Framework, PHP

Topics

Resources

Stars

3 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages