From c8e20b7c2c92cb5eba20c3324f84c17104132f78 Mon Sep 17 00:00:00 2001 From: avl Date: Sun, 17 Aug 2008 05:27:03 +0300 Subject: [PATCH] Initial repo 0.2 alpha --- ipf.php | 135 ++ ipf/admin/app.php | 21 + ipf/admin/media/css/changelists.css | 39 + ipf/admin/media/css/global.css | 323 ++++ ipf/admin/media/css/patch-iewin.css | 7 + ipf/admin/media/css/style.css | 3 + ipf/admin/media/img/arrow-down.gif | Bin 0 -> 80 bytes ipf/admin/media/img/arrow-up.gif | Bin 0 -> 838 bytes ipf/admin/media/img/changelist-bg.gif | Bin 0 -> 58 bytes ipf/admin/media/img/check_off.png | Bin 0 -> 757 bytes ipf/admin/media/img/check_on.png | Bin 0 -> 767 bytes ipf/admin/media/img/chooser-bg.gif | Bin 0 -> 199 bytes ipf/admin/media/img/chooser_stacked-bg.gif | Bin 0 -> 212 bytes ipf/admin/media/img/default-bg-reverse.gif | Bin 0 -> 843 bytes ipf/admin/media/img/default-bg.gif | Bin 0 -> 844 bytes ipf/admin/media/img/hbg.gif | Bin 0 -> 148 bytes ipf/admin/media/img/icon-no.gif | Bin 0 -> 176 bytes ipf/admin/media/img/icon-yes.gif | Bin 0 -> 299 bytes ipf/admin/media/img/icon_addlink.gif | Bin 0 -> 119 bytes ipf/admin/media/img/icon_alert.gif | Bin 0 -> 145 bytes ipf/admin/media/img/icon_calendar.gif | Bin 0 -> 192 bytes ipf/admin/media/img/icon_changelink.gif | Bin 0 -> 119 bytes ipf/admin/media/img/icon_clock.gif | Bin 0 -> 390 bytes ipf/admin/media/img/icon_deletelink.gif | Bin 0 -> 181 bytes ipf/admin/media/img/icon_error.gif | Bin 0 -> 319 bytes ipf/admin/media/img/icon_searchbox.png | Bin 0 -> 667 bytes ipf/admin/media/img/icon_success.gif | Bin 0 -> 341 bytes ipf/admin/media/img/list-add.png | Bin 0 -> 405 bytes ipf/admin/media/img/nav-bg-grabber.gif | Bin 0 -> 116 bytes ipf/admin/media/img/nav-bg-reverse.gif | Bin 0 -> 186 bytes ipf/admin/media/img/nav-bg.gif | Bin 0 -> 273 bytes ipf/admin/media/img/selector-add.gif | Bin 0 -> 606 bytes ipf/admin/media/img/selector-addall.gif | Bin 0 -> 358 bytes ipf/admin/media/img/selector-remove.gif | Bin 0 -> 398 bytes ipf/admin/media/img/selector-removeall.gif | Bin 0 -> 355 bytes ipf/admin/media/img/selector-search.gif | Bin 0 -> 552 bytes ipf/admin/media/img/selector_stacked-add.gif | Bin 0 -> 612 bytes .../media/img/selector_stacked-remove.gif | Bin 0 -> 401 bytes ipf/admin/media/img/tool-left.gif | Bin 0 -> 197 bytes ipf/admin/media/img/tool-left_over.gif | Bin 0 -> 203 bytes ipf/admin/media/img/tool-right.gif | Bin 0 -> 198 bytes ipf/admin/media/img/tool-right_over.gif | Bin 0 -> 200 bytes ipf/admin/media/img/tooltag-add.gif | Bin 0 -> 932 bytes ipf/admin/media/img/tooltag-add_over.gif | Bin 0 -> 336 bytes ipf/admin/media/img/tooltag-arrowright.gif | Bin 0 -> 351 bytes .../media/img/tooltag-arrowright_over.gif | Bin 0 -> 354 bytes ipf/admin/media/img/topbg.gif | Bin 0 -> 167 bytes ipf/admin/model.php | 194 +++ ipf/admin/models.yml | 29 + ipf/admin/models/AdminLog.php | 44 + ipf/admin/models/_generated/BaseAdminLog.php | 34 + ipf/admin/templates/admin/add.html | 25 + ipf/admin/templates/admin/base-simple.html | 16 + ipf/admin/templates/admin/base.html | 34 + ipf/admin/templates/admin/change.html | 27 + ipf/admin/templates/admin/changepassword.html | 26 + ipf/admin/templates/admin/delete.html | 25 + ipf/admin/templates/admin/index.html | 36 + ipf/admin/templates/admin/items.html | 34 + ipf/admin/templates/admin/login.html | 26 + ipf/admin/templates/admin/logout.html | 16 + ipf/admin/views.php | 205 +++ ipf/application.php | 73 + ipf/auth/app.php | 9 + ipf/auth/forms/changepassword.php | 22 + ipf/auth/forms/login.php | 9 + ipf/auth/forms/usercreation.php | 38 + ipf/auth/models.yml | 127 ++ ipf/auth/models/Permission.php | 9 + ipf/auth/models/Role.php | 9 + ipf/auth/models/RolePermission.php | 9 + ipf/auth/models/User.php | 133 ++ ipf/auth/models/UserPermission.php | 9 + ipf/auth/models/UserRole.php | 9 + ipf/auth/models/_generated/BasePermission.php | 30 + ipf/auth/models/_generated/BaseRole.php | 30 + .../models/_generated/BaseRolePermission.php | 27 + ipf/auth/models/_generated/BaseUser.php | 45 + .../models/_generated/BaseUserPermission.php | 27 + ipf/auth/models/_generated/BaseUserRole.php | 27 + ipf/cli.php | 65 + ipf/context.php | 21 + ipf/enum.php | 38 + ipf/exception.php | 3 + ipf/exception/form.php | 3 + ipf/exception/panic.php | 3 + ipf/exception/settings.php | 3 + ipf/exception/template.php | 3 + ipf/form.php | 259 +++ ipf/form/boundfield.php | 101 ++ ipf/form/db.php | 46 + ipf/form/db/boolean.php | 8 + ipf/form/db/email.php | 10 + ipf/form/db/integer.php | 44 + ipf/form/db/string.php | 6 + ipf/form/db/timestamp.php | 8 + ipf/form/field.php | 57 + ipf/form/field/boolean.php | 14 + ipf/form/field/date.php | 27 + ipf/form/field/datetime.php | 42 + ipf/form/field/email.php | 15 + ipf/form/field/file.php | 61 + ipf/form/field/float.php | 28 + ipf/form/field/integer.php | 45 + ipf/form/field/url.php | 18 + ipf/form/field/varchar.php | 30 + ipf/form/fieldproxy.php | 16 + ipf/form/model.php | 80 + ipf/form/widget.php | 46 + ipf/form/widget/checkboxinput.php | 23 + ipf/form/widget/datetimeinput.php | 17 + ipf/form/widget/fileinput.php | 14 + ipf/form/widget/hiddeninput.php | 7 + ipf/form/widget/htmlinput.php | 66 + ipf/form/widget/input.php | 17 + ipf/form/widget/passwordinput.php | 22 + ipf/form/widget/selectinput.php | 34 + ipf/form/widget/selectmultipleinput.php | 46 + .../widget/selectmultipleinputcheckbox.php | 41 + ipf/form/widget/textareainput.php | 23 + ipf/form/widget/textinput.php | 6 + ipf/getopt.php | 197 +++ ipf/http.php | 50 + ipf/http/request.php | 34 + ipf/http/response.php | 103 ++ ipf/http/response/notfound.php | 10 + ipf/http/response/redirect.php | 12 + ipf/http/response/servererror.php | 10 + ipf/http/response/servererrordebug.php | 346 ++++ ipf/http/url.php | 95 ++ ipf/orm.php | 333 ++++ ipf/orm/access.php | 82 + ipf/orm/adapter.php | 74 + ipf/orm/adapter/statement/interface.php | 25 + ipf/orm/builder.php | 18 + ipf/orm/cache/interface.php | 9 + ipf/orm/collection.php | 572 +++++++ ipf/orm/configurable.php | 310 ++++ ipf/orm/connection.php | 848 ++++++++++ ipf/orm/connection/module.php | 29 + ipf/orm/connection/mysql.php | 100 ++ ipf/orm/connection/statement.php | 185 ++ ipf/orm/connection/unitofwork.php | 669 ++++++++ ipf/orm/datadict.php | 50 + ipf/orm/datadict/mysql.php | 373 ++++ ipf/orm/event.php | 194 +++ ipf/orm/eventlistener.php | 38 + ipf/orm/eventlistener/interface.php | 27 + ipf/orm/exception.php | 54 + ipf/orm/exception/adapter.php | 3 + ipf/orm/exception/connection.php | 60 + ipf/orm/exception/locator.php | 3 + ipf/orm/exception/mysql.php | 40 + ipf/orm/exception/orm.php | 54 + ipf/orm/exception/validator.php | 49 + ipf/orm/export.php | 658 ++++++++ ipf/orm/export/mysql.php | 447 +++++ ipf/orm/formatter.php | 132 ++ ipf/orm/hydrator.php | 274 +++ ipf/orm/hydrator/abstract.php | 32 + ipf/orm/hydrator/arraydriver.php | 40 + ipf/orm/hydrator/recorddriver.php | 97 ++ ipf/orm/import/builder.php | 752 +++++++++ ipf/orm/import/schema.php | 542 ++++++ ipf/orm/inflector.php | 315 ++++ ipf/orm/locator.php | 93 + ipf/orm/locator/injectable.php | 57 + ipf/orm/manager.php | 430 +++++ ipf/orm/null.php | 13 + ipf/orm/overloadable.php | 5 + ipf/orm/parser.php | 48 + ipf/orm/parser/spyc.php | 637 +++++++ ipf/orm/parser/yml.php | 19 + ipf/orm/query.php | 1494 +++++++++++++++++ ipf/orm/query/abstract.php | 1058 ++++++++++++ ipf/orm/query/check.php | 101 ++ ipf/orm/query/condition.php | 71 + ipf/orm/query/filter.php | 12 + ipf/orm/query/filter/chain.php | 38 + ipf/orm/query/filter/interface.php | 7 + ipf/orm/query/from.php | 58 + ipf/orm/query/groupby.php | 15 + ipf/orm/query/having.php | 58 + ipf/orm/query/joincondition.php | 126 ++ ipf/orm/query/limit.php | 9 + ipf/orm/query/offset.php | 9 + ipf/orm/query/orderby.php | 16 + ipf/orm/query/parser.php | 5 + ipf/orm/query/part.php | 22 + ipf/orm/query/registry.php | 39 + ipf/orm/query/select.php | 9 + ipf/orm/query/set.php | 27 + ipf/orm/query/tokenizer.php | 235 +++ ipf/orm/query/where.php | 143 ++ ipf/orm/rawsql.php | 230 +++ ipf/orm/record.php | 1266 ++++++++++++++ ipf/orm/record/abstract.php | 192 +++ ipf/orm/record/filter.php | 19 + ipf/orm/record/filter/compound.php | 58 + ipf/orm/record/filter/standard.php | 14 + ipf/orm/record/iterator.php | 28 + ipf/orm/record/listener.php | 22 + ipf/orm/record/listener/chain.php | 151 ++ ipf/orm/record/listener/interface.php | 19 + ipf/orm/relation.php | 183 ++ ipf/orm/relation/association.php | 48 + ipf/orm/relation/foreignkey.php | 56 + ipf/orm/relation/localkey.php | 38 + ipf/orm/relation/nest.php | 84 + ipf/orm/relation/parser.php | 390 +++++ ipf/orm/sequence.php | 21 + ipf/orm/sequence/mysql.php | 52 + ipf/orm/table.php | 1405 ++++++++++++++++ ipf/orm/table/repository.php | 77 + ipf/orm/template.php | 58 + ipf/orm/template/listener/timestampable.php | 48 + ipf/orm/template/timestampable.php | 34 + ipf/orm/transaction.php | 279 +++ ipf/orm/transaction/mysql.php | 47 + ipf/orm/utils.php | 195 +++ ipf/orm/validator.php | 98 ++ ipf/orm/validator/country.php | 260 +++ ipf/orm/validator/creditcard.php | 45 + ipf/orm/validator/date.php | 17 + ipf/orm/validator/driver.php | 46 + ipf/orm/validator/email.php | 11 + ipf/orm/validator/errorstack.php | 79 + ipf/orm/validator/future.php | 43 + ipf/orm/validator/htmlcolor.php | 12 + ipf/orm/validator/ip.php | 9 + ipf/orm/validator/minlength.php | 12 + ipf/orm/validator/nospace.php | 9 + ipf/orm/validator/notblank.php | 9 + ipf/orm/validator/notnull.php | 9 + ipf/orm/validator/past.php | 43 + ipf/orm/validator/range.php | 15 + ipf/orm/validator/readonly.php | 10 + ipf/orm/validator/regexp.php | 25 + ipf/orm/validator/time.php | 27 + ipf/orm/validator/timestamp.php | 30 + ipf/orm/validator/unique.php | 35 + ipf/orm/validator/unsigned.php | 14 + ipf/orm/validator/usstate.php | 69 + ipf/project.php | 94 ++ ipf/router.php | 82 + ipf/session/app.php | 9 + ipf/session/middleware.php | 96 ++ ipf/session/models.yml | 9 + ipf/session/models/Session.php | 47 + ipf/session/models/_generated/BaseSession.php | 16 + ipf/shortcuts.php | 30 + ipf/template.php | 120 ++ ipf/template/compiler.php | 481 ++++++ ipf/template/context.php | 24 + ipf/template/context/request.php | 20 + ipf/template/contextvars.php | 26 + ipf/template/safestring.php | 25 + ipf/template/tag.php | 10 + ipf/template/tag/url.php | 11 + ipf/utils.php | 105 ++ ipf/version.php | 5 + 261 files changed, 23707 insertions(+) create mode 100644 ipf.php create mode 100644 ipf/admin/app.php create mode 100644 ipf/admin/media/css/changelists.css create mode 100644 ipf/admin/media/css/global.css create mode 100644 ipf/admin/media/css/patch-iewin.css create mode 100644 ipf/admin/media/css/style.css create mode 100644 ipf/admin/media/img/arrow-down.gif create mode 100644 ipf/admin/media/img/arrow-up.gif create mode 100644 ipf/admin/media/img/changelist-bg.gif create mode 100644 ipf/admin/media/img/check_off.png create mode 100644 ipf/admin/media/img/check_on.png create mode 100644 ipf/admin/media/img/chooser-bg.gif create mode 100644 ipf/admin/media/img/chooser_stacked-bg.gif create mode 100644 ipf/admin/media/img/default-bg-reverse.gif create mode 100644 ipf/admin/media/img/default-bg.gif create mode 100644 ipf/admin/media/img/hbg.gif create mode 100644 ipf/admin/media/img/icon-no.gif create mode 100644 ipf/admin/media/img/icon-yes.gif create mode 100644 ipf/admin/media/img/icon_addlink.gif create mode 100644 ipf/admin/media/img/icon_alert.gif create mode 100644 ipf/admin/media/img/icon_calendar.gif create mode 100644 ipf/admin/media/img/icon_changelink.gif create mode 100644 ipf/admin/media/img/icon_clock.gif create mode 100644 ipf/admin/media/img/icon_deletelink.gif create mode 100644 ipf/admin/media/img/icon_error.gif create mode 100644 ipf/admin/media/img/icon_searchbox.png create mode 100644 ipf/admin/media/img/icon_success.gif create mode 100644 ipf/admin/media/img/list-add.png create mode 100644 ipf/admin/media/img/nav-bg-grabber.gif create mode 100644 ipf/admin/media/img/nav-bg-reverse.gif create mode 100644 ipf/admin/media/img/nav-bg.gif create mode 100644 ipf/admin/media/img/selector-add.gif create mode 100644 ipf/admin/media/img/selector-addall.gif create mode 100644 ipf/admin/media/img/selector-remove.gif create mode 100644 ipf/admin/media/img/selector-removeall.gif create mode 100644 ipf/admin/media/img/selector-search.gif create mode 100644 ipf/admin/media/img/selector_stacked-add.gif create mode 100644 ipf/admin/media/img/selector_stacked-remove.gif create mode 100644 ipf/admin/media/img/tool-left.gif create mode 100644 ipf/admin/media/img/tool-left_over.gif create mode 100644 ipf/admin/media/img/tool-right.gif create mode 100644 ipf/admin/media/img/tool-right_over.gif create mode 100644 ipf/admin/media/img/tooltag-add.gif create mode 100644 ipf/admin/media/img/tooltag-add_over.gif create mode 100644 ipf/admin/media/img/tooltag-arrowright.gif create mode 100644 ipf/admin/media/img/tooltag-arrowright_over.gif create mode 100644 ipf/admin/media/img/topbg.gif create mode 100644 ipf/admin/model.php create mode 100644 ipf/admin/models.yml create mode 100644 ipf/admin/models/AdminLog.php create mode 100644 ipf/admin/models/_generated/BaseAdminLog.php create mode 100644 ipf/admin/templates/admin/add.html create mode 100644 ipf/admin/templates/admin/base-simple.html create mode 100644 ipf/admin/templates/admin/base.html create mode 100644 ipf/admin/templates/admin/change.html create mode 100644 ipf/admin/templates/admin/changepassword.html create mode 100644 ipf/admin/templates/admin/delete.html create mode 100644 ipf/admin/templates/admin/index.html create mode 100644 ipf/admin/templates/admin/items.html create mode 100644 ipf/admin/templates/admin/login.html create mode 100644 ipf/admin/templates/admin/logout.html create mode 100644 ipf/admin/views.php create mode 100644 ipf/application.php create mode 100644 ipf/auth/app.php create mode 100644 ipf/auth/forms/changepassword.php create mode 100644 ipf/auth/forms/login.php create mode 100644 ipf/auth/forms/usercreation.php create mode 100644 ipf/auth/models.yml create mode 100644 ipf/auth/models/Permission.php create mode 100644 ipf/auth/models/Role.php create mode 100644 ipf/auth/models/RolePermission.php create mode 100644 ipf/auth/models/User.php create mode 100644 ipf/auth/models/UserPermission.php create mode 100644 ipf/auth/models/UserRole.php create mode 100644 ipf/auth/models/_generated/BasePermission.php create mode 100644 ipf/auth/models/_generated/BaseRole.php create mode 100644 ipf/auth/models/_generated/BaseRolePermission.php create mode 100644 ipf/auth/models/_generated/BaseUser.php create mode 100644 ipf/auth/models/_generated/BaseUserPermission.php create mode 100644 ipf/auth/models/_generated/BaseUserRole.php create mode 100644 ipf/cli.php create mode 100644 ipf/context.php create mode 100644 ipf/enum.php create mode 100644 ipf/exception.php create mode 100644 ipf/exception/form.php create mode 100644 ipf/exception/panic.php create mode 100644 ipf/exception/settings.php create mode 100644 ipf/exception/template.php create mode 100644 ipf/form.php create mode 100644 ipf/form/boundfield.php create mode 100644 ipf/form/db.php create mode 100644 ipf/form/db/boolean.php create mode 100644 ipf/form/db/email.php create mode 100644 ipf/form/db/integer.php create mode 100644 ipf/form/db/string.php create mode 100644 ipf/form/db/timestamp.php create mode 100644 ipf/form/field.php create mode 100644 ipf/form/field/boolean.php create mode 100644 ipf/form/field/date.php create mode 100644 ipf/form/field/datetime.php create mode 100644 ipf/form/field/email.php create mode 100644 ipf/form/field/file.php create mode 100644 ipf/form/field/float.php create mode 100644 ipf/form/field/integer.php create mode 100644 ipf/form/field/url.php create mode 100644 ipf/form/field/varchar.php create mode 100644 ipf/form/fieldproxy.php create mode 100644 ipf/form/model.php create mode 100644 ipf/form/widget.php create mode 100644 ipf/form/widget/checkboxinput.php create mode 100644 ipf/form/widget/datetimeinput.php create mode 100644 ipf/form/widget/fileinput.php create mode 100644 ipf/form/widget/hiddeninput.php create mode 100644 ipf/form/widget/htmlinput.php create mode 100644 ipf/form/widget/input.php create mode 100644 ipf/form/widget/passwordinput.php create mode 100644 ipf/form/widget/selectinput.php create mode 100644 ipf/form/widget/selectmultipleinput.php create mode 100644 ipf/form/widget/selectmultipleinputcheckbox.php create mode 100644 ipf/form/widget/textareainput.php create mode 100644 ipf/form/widget/textinput.php create mode 100644 ipf/getopt.php create mode 100644 ipf/http.php create mode 100644 ipf/http/request.php create mode 100644 ipf/http/response.php create mode 100644 ipf/http/response/notfound.php create mode 100644 ipf/http/response/redirect.php create mode 100644 ipf/http/response/servererror.php create mode 100644 ipf/http/response/servererrordebug.php create mode 100644 ipf/http/url.php create mode 100644 ipf/orm.php create mode 100644 ipf/orm/access.php create mode 100644 ipf/orm/adapter.php create mode 100644 ipf/orm/adapter/statement/interface.php create mode 100644 ipf/orm/builder.php create mode 100644 ipf/orm/cache/interface.php create mode 100644 ipf/orm/collection.php create mode 100644 ipf/orm/configurable.php create mode 100644 ipf/orm/connection.php create mode 100644 ipf/orm/connection/module.php create mode 100644 ipf/orm/connection/mysql.php create mode 100644 ipf/orm/connection/statement.php create mode 100644 ipf/orm/connection/unitofwork.php create mode 100644 ipf/orm/datadict.php create mode 100644 ipf/orm/datadict/mysql.php create mode 100644 ipf/orm/event.php create mode 100644 ipf/orm/eventlistener.php create mode 100644 ipf/orm/eventlistener/interface.php create mode 100644 ipf/orm/exception.php create mode 100644 ipf/orm/exception/adapter.php create mode 100644 ipf/orm/exception/connection.php create mode 100644 ipf/orm/exception/locator.php create mode 100644 ipf/orm/exception/mysql.php create mode 100644 ipf/orm/exception/orm.php create mode 100644 ipf/orm/exception/validator.php create mode 100644 ipf/orm/export.php create mode 100644 ipf/orm/export/mysql.php create mode 100644 ipf/orm/formatter.php create mode 100644 ipf/orm/hydrator.php create mode 100644 ipf/orm/hydrator/abstract.php create mode 100644 ipf/orm/hydrator/arraydriver.php create mode 100644 ipf/orm/hydrator/recorddriver.php create mode 100644 ipf/orm/import/builder.php create mode 100644 ipf/orm/import/schema.php create mode 100644 ipf/orm/inflector.php create mode 100644 ipf/orm/locator.php create mode 100644 ipf/orm/locator/injectable.php create mode 100644 ipf/orm/manager.php create mode 100644 ipf/orm/null.php create mode 100644 ipf/orm/overloadable.php create mode 100644 ipf/orm/parser.php create mode 100644 ipf/orm/parser/spyc.php create mode 100644 ipf/orm/parser/yml.php create mode 100644 ipf/orm/query.php create mode 100644 ipf/orm/query/abstract.php create mode 100644 ipf/orm/query/check.php create mode 100644 ipf/orm/query/condition.php create mode 100644 ipf/orm/query/filter.php create mode 100644 ipf/orm/query/filter/chain.php create mode 100644 ipf/orm/query/filter/interface.php create mode 100644 ipf/orm/query/from.php create mode 100644 ipf/orm/query/groupby.php create mode 100644 ipf/orm/query/having.php create mode 100644 ipf/orm/query/joincondition.php create mode 100644 ipf/orm/query/limit.php create mode 100644 ipf/orm/query/offset.php create mode 100644 ipf/orm/query/orderby.php create mode 100644 ipf/orm/query/parser.php create mode 100644 ipf/orm/query/part.php create mode 100644 ipf/orm/query/registry.php create mode 100644 ipf/orm/query/select.php create mode 100644 ipf/orm/query/set.php create mode 100644 ipf/orm/query/tokenizer.php create mode 100644 ipf/orm/query/where.php create mode 100644 ipf/orm/rawsql.php create mode 100644 ipf/orm/record.php create mode 100644 ipf/orm/record/abstract.php create mode 100644 ipf/orm/record/filter.php create mode 100644 ipf/orm/record/filter/compound.php create mode 100644 ipf/orm/record/filter/standard.php create mode 100644 ipf/orm/record/iterator.php create mode 100644 ipf/orm/record/listener.php create mode 100644 ipf/orm/record/listener/chain.php create mode 100644 ipf/orm/record/listener/interface.php create mode 100644 ipf/orm/relation.php create mode 100644 ipf/orm/relation/association.php create mode 100644 ipf/orm/relation/foreignkey.php create mode 100644 ipf/orm/relation/localkey.php create mode 100644 ipf/orm/relation/nest.php create mode 100644 ipf/orm/relation/parser.php create mode 100644 ipf/orm/sequence.php create mode 100644 ipf/orm/sequence/mysql.php create mode 100644 ipf/orm/table.php create mode 100644 ipf/orm/table/repository.php create mode 100644 ipf/orm/template.php create mode 100644 ipf/orm/template/listener/timestampable.php create mode 100644 ipf/orm/template/timestampable.php create mode 100644 ipf/orm/transaction.php create mode 100644 ipf/orm/transaction/mysql.php create mode 100644 ipf/orm/utils.php create mode 100644 ipf/orm/validator.php create mode 100644 ipf/orm/validator/country.php create mode 100644 ipf/orm/validator/creditcard.php create mode 100644 ipf/orm/validator/date.php create mode 100644 ipf/orm/validator/driver.php create mode 100644 ipf/orm/validator/email.php create mode 100644 ipf/orm/validator/errorstack.php create mode 100644 ipf/orm/validator/future.php create mode 100644 ipf/orm/validator/htmlcolor.php create mode 100644 ipf/orm/validator/ip.php create mode 100644 ipf/orm/validator/minlength.php create mode 100644 ipf/orm/validator/nospace.php create mode 100644 ipf/orm/validator/notblank.php create mode 100644 ipf/orm/validator/notnull.php create mode 100644 ipf/orm/validator/past.php create mode 100644 ipf/orm/validator/range.php create mode 100644 ipf/orm/validator/readonly.php create mode 100644 ipf/orm/validator/regexp.php create mode 100644 ipf/orm/validator/time.php create mode 100644 ipf/orm/validator/timestamp.php create mode 100644 ipf/orm/validator/unique.php create mode 100644 ipf/orm/validator/unsigned.php create mode 100644 ipf/orm/validator/usstate.php create mode 100644 ipf/project.php create mode 100644 ipf/router.php create mode 100644 ipf/session/app.php create mode 100644 ipf/session/middleware.php create mode 100644 ipf/session/models.yml create mode 100644 ipf/session/models/Session.php create mode 100644 ipf/session/models/_generated/BaseSession.php create mode 100644 ipf/shortcuts.php create mode 100644 ipf/template.php create mode 100644 ipf/template/compiler.php create mode 100644 ipf/template/context.php create mode 100644 ipf/template/context/request.php create mode 100644 ipf/template/contextvars.php create mode 100644 ipf/template/safestring.php create mode 100644 ipf/template/tag.php create mode 100644 ipf/template/tag/url.php create mode 100644 ipf/utils.php create mode 100644 ipf/version.php diff --git a/ipf.php b/ipf.php new file mode 100644 index 0000000..e3d823c --- /dev/null +++ b/ipf.php @@ -0,0 +1,135 @@ +$val){ + IPF::$settings[strtolower($key)] = $val; + } + } + + private static function loadSettings(){ + $settings_file = IPF::$settings['project_path'].DIRECTORY_SEPARATOR.'settings.php'; + IPF::$settings['settings_file'] = $settings_file; + + if (file_exists($settings_file)) + IPF::applySettings(require $settings_file); + else + throw new IPF_Exception_Settings('Configuration file does not exist: '.$settings_file); + + $settings_local_file = IPF::$settings['project_path'].DIRECTORY_SEPARATOR.'settings_local.php'; + if (file_exists($settings_local_file)) + IPF::applySettings(require $settings_local_file); + + if (!isset(IPF::$settings['dsn'])) + throw new IPF_Exception_Settings('Please specify DSN in settings file'); + else + if (!is_string(IPF::$settings['dsn'])) + throw new IPF_Exception_Settings('DSN must be string'); + + if (!isset(IPF::$settings['tmp'])) + IPF::$settings['tmp'] = '/tmp'; + else + if (!is_string(IPF::$settings['tmp'])) + throw new IPF_Exception_Settings('TMP must be string'); + + if (!isset(IPF::$settings['applications'])) + throw new IPF_Exception_Settings('Please specify application list'); + if (!is_array(IPF::$settings['applications'])) + throw new IPF_Exception_Settings('applications must be array of string'); + + if (!isset(IPF::$settings['template_dirs'])){ + IPF::$settings['template_dirs'] = array(); + IPF::$settings['template_dirs'][] = IPF::$settings['project_path'].DIRECTORY_SEPARATOR.'templates'; + if (array_search('IPF_Admin',IPF::$settings['applications'])) + IPF::$settings['template_dirs'][] = IPF::$settings['ipf_path'].DIRECTORY_SEPARATOR.'ipf'.DIRECTORY_SEPARATOR.'admin'.DIRECTORY_SEPARATOR.'templates'; + } + + if (!isset(IPF::$settings['debug'])){ + IPF::$settings['debug'] = true; + } + + if (!isset(IPF::$settings['app_base'])){ + IPF::$settings['app_base'] = '/index.php'; + } + + if (!isset(IPF::$settings['media_url'])){ + IPF::$settings['media_url'] = '/media/'; + } + + if (!isset(IPF::$settings['admin_media_url'])){ + IPF::$settings['admin_media_url'] = '/ipf/ipf/admin/media/'; + } + + if (!isset(IPF::$settings['urls'])){ + throw new IPF_Exception_Settings('Specify site url routes'); + } + + if (!isset(IPF::$settings['session_cookie_id'])){ + IPF::$settings['session_cookie_id'] = 'sessionid'; + } + + //print_r(IPF::$settings); + } + + public static function boot($ipf_path, $project_path) + { + IPF::$settings['ipf_path']=$ipf_path; + IPF::$settings['project_path']=$project_path; + try{ + IPF::loadSettings(); + }catch(IPF_Exception_Settings $e){ + die('Setting Error: '.$e->getMessage()."\n"); + } + } + + private function __construct(){} + private function __clone(){} + + public static function get($name,$default=null){ + if (isset(IPF::$settings[$name])) + return IPF::$settings[$name]; + return $default; + } + + public static function loadFunction($function) + { + if (function_exists($function)) { + return; + } + $elts = explode('_', $function); + array_pop($elts); + $file = strtolower(implode(DIRECTORY_SEPARATOR, $elts)).'.php'; + @include $file; + + if (!function_exists($function)) + throw new IPF_Exception('Impossible to load the function: '.$function.' in '.$file); + } + + public static function factory($model, $params=null) + { + if ($params !== null) + return new $model($params); + return new $model(); + } +} + +function __($str) +{ + $t = trim($str); + return $t; +} diff --git a/ipf/admin/app.php b/ipf/admin/app.php new file mode 100644 index 0000000..add99d2 --- /dev/null +++ b/ipf/admin/app.php @@ -0,0 +1,21 @@ +array('AdminLog') + )); + } + public static function urls(){ + return array( + array('regex'=>'$#', 'func'=>'IPF_Admin_Views_Index'), + array('regex'=>'([\w\_]+)/([\w\_]+)/$#i', 'func'=>'IPF_Admin_Views_ListItems'), + array('regex'=>'([\w\_]+)/([\w\_]+)/add/$#i', 'func'=>'IPF_Admin_Views_AddItem'), + array('regex'=>'([\w\_]+)/([\w\_]+)/([\w\_]+)/$#i', 'func'=>'IPF_Admin_Views_EditItem'), + array('regex'=>'([\w\_]+)/([\w\_]+)/([\w\_]+)/delete/$#i', 'func'=>'IPF_Admin_Views_DeleteItem'), + array('regex'=>'auth/user/([\w\_]+)/password/$#i', 'func'=>'IPF_Admin_Views_ChangePassword'), + array('regex'=>'login/$#i', 'func'=>'IPF_Admin_Views_Login'), + array('regex'=>'logout/$#i', 'func'=>'IPF_Admin_Views_Logout'), + ); + } +} \ No newline at end of file diff --git a/ipf/admin/media/css/changelists.css b/ipf/admin/media/css/changelists.css new file mode 100644 index 0000000..f187ae3 --- /dev/null +++ b/ipf/admin/media/css/changelists.css @@ -0,0 +1,39 @@ +#changelist { position:relative; width:100%;} +#changelist table { width:100%; } +.change-list .filtered table { border-right:1px solid #ddd; } +.change-list .filtered { min-height:400px; _height:400px; } +.change-list .filtered { background:white url(../img/changelist-bg.gif) top right repeat-y !important; } +.change-list .filtered table, .change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull { margin-right:160px !important; width:auto !important; } +.change-list .filtered table tbody th { padding-right:10px; } +#changelist .toplinks { border-bottom:1px solid #ccc !important; } +#changelist .paginator { color:#666; border-top:1px solid #eee; border-bottom:1px solid #eee; background:white url(../img/nav-bg.gif) 0 180% repeat-x; overflow:hidden; } +.change-list .filtered .paginator { border-right:1px solid #ddd; } + +/* CHANGELIST TABLES */ +#changelist table {border-collapse:collapse; border:1px solid #ABB0BE;} +#changelist table thead th { white-space:nowrap; font-weight:bold; color:#666; padding:2px 5px; font-size:11px; background:url(../img/hbg.gif) top left repeat-x; border-left:1px solid #ABB0BE; border-bottom:1px solid #ABB0BE; } +#changelist table tbody td { border-left: 1px solid #ddd; } +#changelist table tfoot { color: #666; } + +/* TOOLBAR */ +#changelist #toolbar { padding:3px; border-bottom:1px solid #ddd; background:#e1e1e1 url(../img/nav-bg.gif) top left repeat-x; color:#666; } +#changelist #toolbar form input { font-size:11px; padding:1px 2px; } +#changelist #toolbar form #searchbar { padding:2px; } +#changelist #changelist-search img { vertical-align:middle; } + +/* FILTER COLUMN */ +#changelist-filter { position:absolute; top:0; right:0; z-index:1000; width:160px; border-left:1px solid #ddd; background:#efefef; margin:0; } +#changelist-filter h2 { font-size:11px; padding:2px 5px; border-bottom:1px solid #ddd; } +#changelist-filter h3 { font-size:12px; margin-bottom:0; } +#changelist-filter ul { padding-left:0;margin-left:10px;_margin-right:-10px; } +#changelist-filter li { list-style-type:none; margin-left:0; padding-left:0; } +#changelist-filter a { color:#999; } +#changelist-filter a:hover { color:#036; } +#changelist-filter li.selected { border-left:5px solid #ccc; padding-left:5px;margin-left:-10px; } +#changelist-filter li.selected a { color:#5b80b2 !important; } + +/* DATE DRILLDOWN */ +.change-list ul.toplinks { display:block; background:white url(../img/nav-bg-reverse.gif) 0 -10px repeat-x; border-top:1px solid white; float:left; padding:0 !important; margin:0 !important; width:100%; } +.change-list ul.toplinks li { float: left; width: 9em; padding:3px 6px; font-weight: bold; list-style-type:none; } +.change-list ul.toplinks .date-back a { color:#999; } +.change-list ul.toplinks .date-back a:hover { color:#036; } diff --git a/ipf/admin/media/css/global.css b/ipf/admin/media/css/global.css new file mode 100644 index 0000000..ee6e9d1 --- /dev/null +++ b/ipf/admin/media/css/global.css @@ -0,0 +1,323 @@ +body.dashboard { margin:0; padding:0; font-family:"Lucida Grande","Bitstream Vera Sans",Verdana,Arial,sans-serif; color:#333; background:url('../img/topbg.gif') left top repeat-x; } +body.simple { margin:10; padding:10; font-family:"Lucida Grande","Bitstream Vera Sans",Verdana,Arial,sans-serif; color:#333; } + +a:link, a:visited { color: #005681; text-decoration:none; } +a:hover { color: #444444; text-decoration:underline; } +a img { border:none; } + +p, ol, ul, dl { margin:.2em 0 .8em 0; font-size:12px; } +p { padding:0; line-height:140%; } +h1,h2,h3,h4,h5 { font-weight:bold; } +h1 { font-weight:normal; font-size:18px; color:#666; padding:0 6px 0 0; margin:0 0 .2em 0; } +h2 { font-size:16px; margin:1em 0 .5em 0; } +h2.subhead { font-weight:normal;margin-top:0; } +h3 { font-size:14px; margin:.8em 0 .3em 0; color:#666; font-weight:bold; } +h4 { font-size:12px; margin:1em 0 .8em 0; padding-bottom:3px; } +h5 { font-size:10px; margin:1.5em 0 .5em 0; color:#666; text-transform:uppercase; letter-spacing:1px; } +ul li { list-style-type:square; padding:1px 0; } +ul.plainlist { margin-left:0 !important; } +ul.plainlist li { list-style-type:none; } +li ul { margin-bottom:0; } +li, dt, dd { font-size:11px; line-height:14px; } +dt { font-weight:bold; margin-top:4px; } +dd { margin-left:0; } +form { margin:0; padding:0; } +fieldset { margin:0; padding:0; } +blockquote { font-size:11px; color:#777; margin-left:2px; padding-left:10px; border-left:5px solid #ddd; } +code, pre { font-family:"Bitstream Vera Sans Mono", Monaco, "Courier New", Courier, monospace; background:inherit; color:#666; font-size:11px; } +pre.literal-block { margin:10px; background:#eee; padding:6px 8px; } +code strong { color:#930; } +hr { clear:both; color:#eee; background-color:#eee; height:1px; border:none; margin:0; padding:0; font-size:1px; line-height:1px; } + + + +/* PAGE STRUCTURE */ +#ipfcontainer { position:relative; width:100%; min-width:760px; } +#ipfcontent { margin:10px 10px; } +#ipfheader { width:100%;} +#ipfcontent-main { float:left; width:100%; } +#ipfcontent-related { float:right; width:220px; position:relative; margin-right:-230px; } +#ipffooter { clear:both; padding:10px; } +#ipfver {margin-left:10px; font-size:.8em; color:#999;} + +/* COLUMN TYPES */ +.colMS { margin-right:245px !important; } +.colSM { margin-left:245px !important; } +.colSM #ipfcontent-related { float:left; margin-right:0; margin-left:-230px; } +.colSM #ipfcontent-main { float:right; } +.popup .colM { width:95%; } +.subcol { float:left; width:46%; margin-right:15px; } +.dashboard #content { width:500px; } + +/* HEADER */ +#ipfheader { overflow:hidden; height:43px; } +/* #417690 #ffc #33436a*/ +#ipfheader a:hover { text-decoration:underline; } +#ipfbranding h1 { padding:10px 10px 0 10px; font-size:18px; margin:0; font-weight:normal; color:#444444; } +#ipfuser-tools { position:absolute; top:0; right:0; padding:1.2em 10px; font-size:11px; text-align:right; } + +/* SIDEBAR */ +#ipfcontent-related h3 { font-size:12px; color:#666; margin-bottom:3px; } +#ipfcontent-related h4 { font-size:11px; } + +/* TABLES */ +table { border-collapse:collapse; border-color:#ABB0BE; } +td, th { font-size:11px; line-height:13px; border-bottom:1px solid #eee; vertical-align:top; padding:5px; font-family:"Lucida Grande", Verdana, Arial, sans-serif; } +th { text-align:left; font-size:12px; } +thead th { font-weight:bold; color:#666; padding:2px 5px; font-size:11px; background:url(../img/hbg.gif) top left repeat-x; border-left:1px solid #ABB0BE; border-bottom:1px solid #ABB0BE; } +thead th:first-child { border-left:none !important; } +.superwide table th, .superwide table td, .superwide table input, .superwide table select { font-size:10px; } +.module table { border-collapse: collapse; } +thead th.optional { font-weight:normal !important; } +#ipfhome-page table.module tr:hover { background:#EDF3FE; } +fieldset { border:1px solid #eee; } +fieldset table { width:100%; } +fieldset table th{ width:100px; } +tr.row-label td { font-size:9px; padding-top:2px; padding-bottom:0; border-bottom:none; color:#666; margin-top:-1px; } +tr.alt { background:#f6f6f6; } +.row1 { background:#EDF3FE; } +.row2 { background:white; } +table#ipfchange-history { width:100%; } +table#ipfchange-history tbody th { width:16em; } + +/* TABLE SORTING */ +thead th a:link, thead th a:visited { color:#666; display:block; } +table thead th.sorted { background-position:bottom left !important; } +table thead th.sorted a { padding-right:13px; } +table thead th.ascending a { background:url(../img/arrow-down.gif) right .4em no-repeat; } +table thead th.descending a { background:url(../img/arrow-up.gif) right .4em no-repeat; } + +/* MODULES */ +.module { border:1px solid #ABB0BE; margin-bottom:5px; } +.module p, .module ul, .module h3, .module h4, .module dl, .module pre { padding-left:10px; padding-right:10px; } +.module blockquote { margin-left:12px; } +.module ul, .module ol { margin-left:1.5em; } +.module h2, .module caption { border-bottom:1px solid #ABB0BE; margin:0; padding:2px 5px 3px 5px; font-size:11px; text-align:left; background:url(../img/hbg.gif) left top repeat-x; color:#444444; font-weight:bold; } +.module caption { border:1px solid #ABB0BE; } +.module h3 { margin-top:.6em; } +#ipfcontent-related .module h2 { background:#eee url(../img/nav-bg.gif) bottom left repeat-x; color:#666; } +#ipfcontent-main .verbose .actionlist { float:right; font-size:10px; width:17em; position:relative; top:-1.6em; margin:0 8px; } + +/* DASHBOARD */ +.dashboard .module table th { width:100%; } +.dashboard .module table td { white-space:nowrap; } +.dashboard .module table td a { display:block; padding-right:.6em; } + +/* RECENT ACTIONS MODULE */ +.module ul.actionlist { margin-left:0; } +ul.actionlist li { list-style-type:none; } + +/* FORM DEFAULTS */ +input, textarea, select { margin:2px 0; padding:2px 3px; vertical-align:middle; border:1px solid #ABB0BE; font-family:"Lucida Grande", Verdana, Arial, sans-serif; font-weight:normal; font-size:11px; } +textarea { vertical-align:top !important; } +input[type=checkbox], input[type=radio] { border:none; } + +/* FORM BUTTONS */ +input[type=submit], input[type=button], .submit-row input { background:white url(../img/nav-bg.gif) bottom repeat-x; padding:2px; color:black; } +input[type=submit]:active, input[type=button]:active { background-image:url(../img/nav-bg.gif); background-position:bottom; } +input[type=submit].default, .submit-row input.default { border:2px solid #888; background: url(../img/hbg.gif) center repeat-x; font-weight:bold; color:#888; } +input[type=submit].default:active { background-image:url(../img/default-bg-reverse.gif); background-position:top; } +.submit-row { padding:5px 7px; text-align:right; background:url(../img/topbg.gif) top left repeat-x; border:1px solid #ABB0BE; margin:5px 0; } +.submit-row input { margin:0 0 0 5px; } +.submit-row .float-left { padding-top:.1em; } + +/* FORM ROWS */ +.ipfform-row { clear:both; padding:8px 12px; font-size:11px; } +html>body .ipfform-row { border-bottom:1px solid #eee; } +.ipfform-row:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } +.ipfform-row img, .ipfform-row input { vertical-align:middle; } +form .form-row p { padding-left:0; font-size:11px; } + +/* FORM LABELS */ +form h4 { margin:0 !important; padding:0 !important; border:none !important; } +label { font-weight:normal !important; color:#666; font-size:12px; } +label.inline { margin-left:20px; } +.required label, label.required { font-weight:bold !important; color:#333 !important; } + +/* RADIO BUTTONS */ +form ul.radiolist li { list-style-type:none; } +form ul.radiolist label { float:none; display:inline; } +form ul.inline { margin-left:0; padding:0; } +form ul.inline li { float:left; padding-right:7px; } + +/* ALIGNED FIELDSETS */ +.aligned label { display:block; padding:0 1em 3px 0; float:left; text-align:left; width:8em; } +.aligned label.inline { display:inline; float:none; } +.colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField { width:350px; } +form .aligned p, form .aligned ul { margin-left:7em; padding-left:30px; } +form .aligned table p { margin-left:0; padding-left:0; } +form .aligned p.help { padding-left:38px; } +.aligned .vCheckboxLabel { float:none !important; display:inline; } +.colM .aligned .vLargeTextField, colM .aligned .vXMLLargeTextField { width:610px; } +.checkbox-row p.help { margin-left:0; padding-left:0 !important; } + +/* WIDE FIELDSETS */ +.wide label { width:15em !important; } +form .wide p { margin-left:15em; } +form .wide p.help { padding-left:38px; } +.colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField { width:450px; } + +/* COLLAPSED FIELDSETS */ +fieldset.collapsed * { display:none; } +fieldset.collapsed h2, fieldset.collapsed { display:block !important; } +fieldset.collapsed .collapse-toggle { display: inline !important; } +fieldset.collapse h2 a.collapse-toggle { color:#ffc; } +fieldset.collapse h2 a.collapse-toggle:hover { text-decoration:underline; } +.hidden { display:none; } + +/* MONOSPACE TEXTAREAS */ +fieldset.monospace textarea { font-family:"Bitstream Vera Sans Mono",Monaco,"Courier New",Courier,monospace; } + +/* MESSAGES & ERRORS */ +ul.messagelist { padding:0 0 5px 0; margin:0; } +ul.messagelist li { font-size:12px; display:block; padding:4px 5px 4px 25px; margin:0 0 3px 0; border-bottom:1px solid #ddd; color:#666; background:#ffc url(../img/icon_success.gif) 5px .3em no-repeat; } +.errornote { font-size:12px !important; display:block; padding:4px 5px 4px 25px; margin:0 0 3px 0; border:1px solid red; color:red;background:#ffc url(../img/icon_error.gif) 5px .3em no-repeat; } +ul.errorlist { margin:0 !important; padding:0 !important; } +.errorlist li { font-size:12px !important; display:block; padding:4px 5px 4px 25px; margin:0 0 3px 0; border:1px solid maroon; color:maroon; background:#FEE url(../img/icon_alert.gif) 3px 2px no-repeat; } +td ul.errorlist { margin:0 !important; padding:0 !important; } +td ul.errorlist li { margin:0 !important; } +.error { background:#ffc; } +.error input, .error select { border:1px solid red; } +div.system-message { background: #ffc; margin: 10px; padding: 6px 8px; font-size: .8em; } +div.system-message p.system-message-title { padding:4px 5px 4px 25px; margin:0; color:red; background:#ffc url(../img/icon_error.gif) 5px .3em no-repeat; } + +/* ACTION ICONS */ +.addlink { padding-left:12px; background:url(../img/icon_addlink.gif) 0 .2em no-repeat; } +.changelink { padding-left:12px; background:url(../img/icon_changelink.gif) 0 .2em no-repeat; } +.deletelink { padding-left:12px; background:url(../img/icon_deletelink.gif) 0 .2em no-repeat; } +a.deletelink:link, a.deletelink:visited { color:#CC3434; } +a.deletelink:hover { color:#993333; } + +/* OBJECT TOOLS */ +.object-tools { font-size:10px; font-weight:bold; font-family:Arial,Helvetica,sans-serif; padding-left:0; margin-bottom:5px; float:right; position:relative; margin-top:-2.4em; margin-bottom:-2em; } +.form-row .object-tools { margin-top:0; margin-bottom:0; } +.object-tools li { display:block; float:left; background:url(../img/tool-left.gif) 0 0 no-repeat; padding:0 0 0 8px; margin-left:2px; height:16px; } +.object-tools li:hover { background:url(../img/tool-left_over.gif) 0 0 no-repeat; } +.object-tools a:link, .object-tools a:visited { display:block; float:left; color:white; padding:.1em 14px .1em 8px; height:14px; background:#999 url(../img/tool-right.gif) 100% 0 no-repeat; } +.object-tools a:hover, .object-tools li:hover a { background:#5b80b2 url(../img/tool-right_over.gif) 100% 0 no-repeat; } +.object-tools a.viewsitelink, .object-tools a.golink { background:#999 url(../img/tooltag-arrowright.gif) top right no-repeat; padding-right:28px; } +.object-tools a.viewsitelink:hover, .object-tools a.golink:hover { background:#5b80b2 url(../img/tooltag-arrowright_over.gif) top right no-repeat; } +.object-tools a.addlink { background:#999 url(../img/tooltag-add.gif) top right no-repeat; padding-right:28px; } +.object-tools a.addlink:hover { background:#5b80b2 url(../img/tooltag-add_over.gif) top right no-repeat; } + +/* INLINE CONTROLS */ +#ipfinline-controls { font-weight:bold; font-size:12px; } +#ipfinline-specific-controls { margin-left:6px; padding:0 8px; border-left:6px solid #ABB0BE; } + +/* BREADCRUMBS */ +div.breadcrumbs { padding:2px 8px 3px 10px; font-size:11px; color:#444; text-align:left; } +div.breadcrumbs a{ text-decoration:none;} +div.breadcrumbs a:hover;{ text-decoration:underline; color:#444; } + +/* SELECTOR (FILTER INTERFACE) */ +.selector { width:580px; float:left; } +.selector select { width:270px; height:170px; } +.selector-available, .selector-chosen { float:left; width:270px; text-align:center; margin-bottom:5px; } +.selector-available h2, .selector-chosen h2 { border:1px solid #ABB0BE; } +.selector .selector-available h2 { background:white url(../img/nav-bg.gif) bottom left repeat-x; color:#666; } +.selector .selector-filter { background:white; border:1px solid #ABB0BE; border-width:0 1px; padding:3px; color:#999; font-size:10px; margin:0; text-align:left; } +.selector .selector-chosen .selector-filter { padding:4px 5px; } +.selector .selector-available input { width:230px; } +.selector ul.selector-chooser { float:left; width:22px; height:50px; background:url(../img/chooser-bg.gif) top center no-repeat; margin:13% 3px 0 3px; padding:0; } +.selector-chooser li { margin:0; padding:3px; list-style-type:none; } +.selector select { margin-bottom:5px; margin-top:0; } +.selector-add, .selector-remove { width:16px; height:16px; display:block; text-indent:-3000px; } +.selector-add { background:url(../img/selector-add.gif) top center no-repeat; margin-bottom:2px; } +.selector-remove { background:url(../img/selector-remove.gif) top center no-repeat; } +a.selector-chooseall, a.selector-clearall { display:block; width:6em; text-align:left; margin-left:auto; margin-right:auto; font-weight:bold; color:#666; padding:3px 0 3px 18px; } +a.selector-chooseall:hover, a.selector-clearall:hover { color:#036; } +a.selector-chooseall { width:7em; background:url(../img/selector-addall.gif) left center no-repeat; } +a.selector-clearall { background:url(../img/selector-removeall.gif) left center no-repeat; } + +/* Stacked selectors for long items */ +.stacked { float:left; width:500px; } +.stacked select { width:480px; height:100px; } +.stacked .selector-available, .stacked .selector-chosen { width:480px; } +.stacked .selector-available { margin-bottom:0; } +.stacked .selector-available input { width:442px; } +.stacked ul.selector-chooser { height:22px; width:50px; margin:0 0 3px 40%; background:url(../img/chooser_stacked-bg.gif) top center no-repeat; } +.stacked .selector-chooser li { float:left; padding:3px 3px 3px 5px; } +.stacked .selector-chooseall, .stacked .selector-clearall { display:none; } +.stacked .selector-add { background-image:url(../img/selector_stacked-add.gif); } +.stacked .selector-remove { background-image:url(../img/selector_stacked-remove.gif); } + +/* DATE AND TIME */ +p.datetime { line-height:20px; margin:0; padding:0; color:#666; font-size:11px; font-weight:bold; } +.datetime span { font-size:11px; font-weight:normal; color:#ABB0BE; white-space:nowrap; } +.vDateField { margin-left:4px; } +table p.datetime { font-size:10px; margin-left:0; padding-left:0; } + +/* FILE UPLOADS */ +p.file-upload { line-height:20px; margin:0; padding:0; color:#666; font-size:11px; font-weight:bold; } +.file-upload a { font-weight:normal; } +.file-upload .deletelink { margin-left:5px; } + +/* CALENDARS & CLOCKS */ +.calendarbox, .clockbox { margin:5px auto; font-size:11px; width: 16em; text-align: center; background:white; position:relative; } +.clockbox { width:9em; } +.calendar { margin:0; padding: 0; } +.calendar table { margin: 0; padding: 0; border-collapse:collapse; background:white; width:99%; } +.calendar caption, .calendarbox h2 { margin: 0; font-size:11px; text-align:center; border-top:none; } +.calendar th { font-size:10px; color:#666; padding:2px 3px; text-align:center; background:#e1e1e1 url(../img/nav-bg.gif) 0 50% repeat-x; border-bottom:1px solid #ddd; } +.calendar td { font-size:11px; text-align: center; padding: 0; border-top:1px solid #eee; border-bottom:none; } +.calendar td.selected a { background: #C9DBED; } +.calendar td.nonday { background:#efefef; } +.calendar td.today a { background:#ffc; } +.calendar td a, .timelist a { display: block; font-weight:bold; padding:4px; text-decoration: none; color:#444; } +.calendar td a:hover, .timelist a:hover { background: #5b80b2; color:white; } +.calendar td a:active, .timelist a:active { background: #036; color:white; } +.calendarnav { font-size:10px; text-align: center; color:#ABB0BE; margin:0; padding:1px 3px; } +.calendarnav a:link, #calendarnav a:visited, #calendarnav a:hover { color: #999; } +.calendar-shortcuts { background:white; font-size:10px; line-height:11px; border-top:1px solid #eee; padding:3px 0 4px; color:#ABB0BE; } +.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next { display:block; position:absolute; font-weight:bold; font-size:12px; background:#C9DBED url(../img/default-bg.gif) bottom left repeat-x; padding:1px 4px 2px 4px; color:white; } +.calendarnav-previous:hover, .calendarnav-next:hover { background:#036; } +.calendarnav-previous { top:0; left:0; } +.calendarnav-next { top:0; right:0; } +.calendar-cancel { margin:0 !important; padding:0; font-size:10px; background:#e1e1e1 url(../img/nav-bg.gif) 0 50% repeat-x; border-top:1px solid #ddd; } +.calendar-cancel a { padding:2px; color:#999; } +ul.timelist, .timelist li { list-style-type:none; margin:0; padding:0; } +.timelist a { padding:2px; } + +/* ORDERING WIDGET */ +ul#orderthese { position:absolute; top:8em; right:0; width:240px; padding:0; margin:0; list-style-type:none; } +ul#orderthese li { list-style-type:none; display:block; padding:0; margin:6px 0; width:214px; background:#f6f6f6; white-space:nowrap; overflow:hidden; } +ul#orderthese li span { display:block; border:1px solid #e7e7e7; background:transparent url(../img/nav-bg-grabber.gif) top left repeat-y; font-size:10px !important; padding:4px 6px 4px 12px; } +ul#orderthese span:hover { background-color:#efefef; } + +/* PAGINATOR */ +.paginator { font-size:11px; padding-top:10px; padding-bottom:10px; line-height:22px; margin:0; border-top:1px solid #ddd; } +.paginator a:link, .paginator a:visited { padding:2px 6px; border:solid 1px #ABB0BE; background:white; text-decoration:none; } +.paginator a.showall { padding:0 !important; border:none !important; } +.paginator a.showall:hover { color:#036 !important; background:transparent !important; } +.paginator .end { border-width:2px !important; margin-right:6px; } +.paginator .this-page { padding:2px 6px; font-weight:bold; font-size:13px; vertical-align:top; } +.paginator a:hover { color:white; background:#5b80b2; border-color:#036; } + +/* TEXT STYLES & MODIFIERS */ +.small { font-size:11px; } +.tiny { font-size:10px; } +p.tiny { margin-top:-2px; } +.mini { font-size:9px; } +p.mini { margin-top:-3px; } +.help, p.help { font-size:10px !important; color:#999; } +p img, h1 img, h2 img, h3 img, h4 img, td img { vertical-align:middle; } +.quiet, a.quiet:link, a.quiet:visited { color:#999 !important;font-weight:normal !important; } +.quiet strong { font-weight:bold !important; } +.float-right { float:right; } +.float-left { float:left; } +.clear { clear:both; } +.align-left { text-align:left; } +.align-right { text-align:right; } +.example { margin:10px 0; padding:5px 10px; background:#efefef; } +.nowrap { white-space:nowrap; } + +/* CUSTOM FORM FIELDS */ +.vSelectMultipleField { vertical-align:top !important; } +.vCheckboxField { border:none; } +.vDateField, .vTimeField { margin-right:2px; } +.vFileUploadField { border:none; } +.vURLField { width:380px; } +.vLargeTextField, .vXMLLargeTextField { width:480px; } +.colM .vLargeTextField, .colM .vXMLLargeTextField { width:720px; } +body.core-flatfile #id_content { height: 400px; } +.module table .vPositiveSmallIntegerField { width: 22px; } \ No newline at end of file diff --git a/ipf/admin/media/css/patch-iewin.css b/ipf/admin/media/css/patch-iewin.css new file mode 100644 index 0000000..4653132 --- /dev/null +++ b/ipf/admin/media/css/patch-iewin.css @@ -0,0 +1,7 @@ +* html #container { position:static; } /* keep header from flowing off the page */ +* html .colMS #content-related { margin-right:0; margin-left:10px; position:static; } /* put the right sidebars back on the page */ +* html .colSM #content-related { margin-right:10px; margin-left:-115px; position:static; } /* put the left sidebars back on the page */ +* html .dashboard #content { width:768px; } /* proper fixed width for dashboard in IE6 */ +* html .dashboard #content-main { width:535px; } /* proper fixed width for dashboard in IE6 */ +* html #content { width /**/: 768px; } /* fixed width for IE5 */ +* html #content-main { width /**/: 535px; } /* fixed width for IE5 */ \ No newline at end of file diff --git a/ipf/admin/media/css/style.css b/ipf/admin/media/css/style.css new file mode 100644 index 0000000..7c4437f --- /dev/null +++ b/ipf/admin/media/css/style.css @@ -0,0 +1,3 @@ +@import url(global.css); +@import url(changelists.css); +/*\*/ @import "patch-iewin.css"; /**/ \ No newline at end of file diff --git a/ipf/admin/media/img/arrow-down.gif b/ipf/admin/media/img/arrow-down.gif new file mode 100644 index 0000000000000000000000000000000000000000..a967b9fd5563a0fc2f5fde8ec0f7de3fc8fbc5a9 GIT binary patch literal 80 zcmZ?wbhEHb3wfn>L+1d2-{%jW1rjm^pLi|Ns9P7#I|PvM@3LmFNK3 i3?Q`(%%TyyyiA!oB04G)Te5yh$2@OMO7G-kum%ACMICAY literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/arrow-up.gif b/ipf/admin/media/img/arrow-up.gif new file mode 100644 index 0000000000000000000000000000000000000000..3fe4851399a37337891ccf5452ee5d59bbedeec7 GIT binary patch literal 838 zcmZ?wbhEHb3wfn>L+1d2-{%jW1rjm^pJM!zdUHfe{k|ia%Kx8GuSOX;Q=NFCLWFl0*@NH#f6pb?3n1_z+epkX4f1j literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/changelist-bg.gif b/ipf/admin/media/img/changelist-bg.gif new file mode 100644 index 0000000000000000000000000000000000000000..7f4699470adc8c021740023dcd4f0a1bd5f50b84 GIT binary patch literal 58 zcmZ?wbhEHbT*$!0(7?d({{8#Acke3xWMKdS9S{MMVPF#E>05dFE&t*eBi*} z<;(8_(UK)k=gAsp&TZ zga3MZ?+pz<8W?=l*Z;1o`(H=rt%1Qyef{S^wwl_1b@l(Ms@Dw+{;8?`1#*>@|0^i` z10s3(|FW|GWn^xvsr{3dz7CXeCyh^(_P1$XVbKS~KsCcHQi(^Q|9MQ8^`3^ZSxLiD#T3CKPfI}p`Ihf;Jl){Vu z|CvtTwmqrB?9`zemb>hYjzh`8lxfTNW}Z7B&|$s!ZsP5bmBKTG^G+BE^X63ZE6FTx z3gN8$Xu)*;@;<2vB6^EByjUOhPpa6jZ2KbPpRwy})8=Hkwmo|;9{T#R@yYguw_a}B wyl(m%_xl!e)}Q}go|>X?#Co5dSe=-??G3S$`|ihX13Hqy)78&qol`;+01+UAM*si- literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/check_on.png b/ipf/admin/media/img/check_on.png new file mode 100644 index 0000000000000000000000000000000000000000..079d930b326d284e35a292d1cb1cfdc1f6b4dd5f GIT binary patch literal 767 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$3?vg*uel1On2Vh}LpV4%Za?&Yz`($05#STz z`v3p`FF!wi{`uk4kN2N`zJKuk#=ZB~?!LQn=k1kSZ!X?^bN=kPGeEHU+M;78j~qR5 z&Lnb;e&ia1h}C+r%grNJ z8icLZ30$ETxI#5*iD|%cRsZG6p^NpymgxH~Q}9_T@3%xbbiRJbJblk4G9mNz-4{u@ zEs_YDtLwTz!ePFE!+b%9xk9${c-*He*v{hjo~&juo84=on$>h34*y zjCxsIo7D6=7o)O4B|w3`^T8yU3f7_@2`G;0_%s~I$_7&Iyw)XNyu ziW$@j8B_}xRB{+pvKc-q+&2IQX+}wqUobEX;eY|oqCVsF|LX}rJ2?wHB8wRqxP?HN z@zUM8KR`jT64!_l=c3falFa-(g^UaF zqc`l{uv<7Xq4BeP)A{|i`}h=E&fIg-OgzZ2WNZ87kd2PdzgXn#^VoLl?e=H3ENb-= z-t;Zs61Ba!NnMm%$6T5BL>l*kM_ige%V#Gpm=V|%!sjygp@oN;wibg@{iFNU8~D`u zCS1|mqtL=I^?G#q^M67|wf{-5wHyEbQ2MMY9TZUv9~%Sp!>_)cW&ci3#* zUhC5m*S~nZz=*>R=VoO!-wlwA$Z`-01 q@~q#j3=%veq*@rvq^WHhU}-`1SSm{{H^+^Yi-p`uF$u@bK{c{r&s< z`||Sg^z`)c@$vTd_5c6?A^8LV00000EC2ui05AX-000DmFvuy15jgA3yCGB Oo}Zwd4gw#T5db^5xM#Bf literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/default-bg-reverse.gif b/ipf/admin/media/img/default-bg-reverse.gif new file mode 100644 index 0000000000000000000000000000000000000000..0873281e51bdcfd1e7eb5bbfbde3a09c4cbc943e GIT binary patch literal 843 zcmV-R1GM}{Nk%w1VG#fy0OkMyywd5g$>Wc((SfDPyV2;h%;kux&VHiEgQv@Yq{*Sa z+l{W!qQ2X>&*yuf#)zuThN#S(y4aey*L$GHimcC{yxF9{+^)ysgs01&yxO+S=DE=5 zq`=*$!rrpW%Ia#o)Ef<&3V-tHt57%Hx^1)~CYVyVB@=p~tMo;ew{h zv&!VG#o?8+)Qqmrnz+`6sm*|+$&#|ts>I*7&E~4Z;GetNkg(B_veJR3%8#$nw9VzU z&E=W6*PXi9oVwYCsLX|_%&NoRhN#WD(ddt_(zwp%xX$ON!QH9E;Dn~jv&-bS&gP%H z*|yE)hN;YZp~kn*=6s;Xx6kLIz1xql(y__nrNG^btIvL;$&0MdA^8LV00000EC2ui z01*Hm000O7fOifPgAR3s6N!Zm4lp*4BuykFJTOf*l}$X9EG!3~2cDpzoqh=-BB!UR zRtX8Ms!u=zUbD1M1G%=j2p9;zy>Gw17{$G1Xb@%)%w}{DRc2LX%n%O-*9O`T+}qs- z6yY>9I~3y-JAD*?=sPhn3-R$WM@RDTQ%3>JcA0syKJpvRw!9yxMc>2a`&3xzov zx=8UshKv*xrBwWoF@=l|HCC*s;bR3396o&D2#Io}gqKQ&P@xj@r5G_1lt>sdX2ggH z5q<(4L&hf=pb;u$$grh?Ql=T4R*EFeIr5KLGLmcd%i6bbUe36dy_8@~t=1<8vdNG`N! zQKH3#5}!MR)_mbYfB-X2r#`*swCdC*JV5b~Bz9~MxJ{UV0|gEkBL#wo0UoA+5ir4% zYY4HNd5A1o;>^)NJp}VL4YJ!gaiT$Y@80pSX|Q3#jTWc((SfDPyV2;h%;kux&VHiEgQv@Yq{*Sa z+l{W!qQ2X>&*yuf#)zuThN#S(y4aey*L$GHimcC{yxF9{+^)ysgs01&yxO+S=DE=5 zq`=*$!rrpW%Ia#o)Ef<&3V-tHt57%Hx^1)~CYVyVB@=p~tMo;ew{h zv&!VG#o?8+)Qqmrnz+`6sm*|+$&#|ts>I*7&E~4Z;GetNkg(B_veJR3%8#$nw9VzU z&E=W6*PXi9oVwYCsLX|_%&NoRhN#WD(ddt_(zwp%xX$ON!QH9E;Dn~jv&-bS&gP%H z*|yE)hN;YZp~kn*=6s;Xx6kLIz1xql(y__nrNG^btIvL;$&0MdA^8LV00000EC2ui z01*Hm000O7fB=GngoT0x1WZIsh=@c4X^)Hq8yhz_C@7YhmN%6fC~FO)b|)tdr>LlS zYz-PAudpFmS#fh3xFN7$Mg_iMy zEiEN27bWH6=HM3>ML{o4NKx(YFF{dAFGWZyZdf27DgX-9f+e7qGeUGM>CmP_hlM&i z=nx`A;t~!VesHj0A)^I895r&}pnw5`k|<3oSa~uJg9;fkY^eZKW(JxS@T~!jKv(R@CsZ0tXHsK5$_Dsx`$288T9sm2&pO z+7vRWKDg+SBgd5H2ZJ>LS5P{( literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/icon-no.gif b/ipf/admin/media/img/icon-no.gif new file mode 100644 index 0000000000000000000000000000000000000000..1b4ee5814570885705399533f1182f8b0491c5fb GIT binary patch literal 176 zcmZ?wbhEHb`H-TFR%!C^)o_GDj!gPtK1gc27Dv@$SQ0{~`FJvsmY literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/icon-yes.gif b/ipf/admin/media/img/icon-yes.gif new file mode 100644 index 0000000000000000000000000000000000000000..73992827403791d6c1a75a079880e41dce7e0214 GIT binary patch literal 299 zcmZ?wbhEHbb?NhTQ$x_deWPc4O)NkN2|oXRf%p{M+wuUw(Z# z`TWGXJ8Mf07p=Or^7yl3mtJ2C+~V)C-fh~&DX}}E_C4PF@Y93ee}B)tGUw-?pC_Il zZ#vO%{oS?y|Nqw=uUUR`+4?){5_iQh&Q{xM6OkFieY2o T4)tf0@^WEj=4)bdWUvMRbX#E6 literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/icon_addlink.gif b/ipf/admin/media/img/icon_addlink.gif new file mode 100644 index 0000000000000000000000000000000000000000..ee70e1adba52480cc6aedbee650000c5d55b0088 GIT binary patch literal 119 zcmZ?wbhEHb(s)E@aY^3 F)&O8RB1ZrK literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/icon_alert.gif b/ipf/admin/media/img/icon_alert.gif new file mode 100644 index 0000000000000000000000000000000000000000..a1dde2625445b76d041ae02ccfcb83481ca63c5e GIT binary patch literal 145 zcmV;C0B-+BNk%w1VGsZi0J9GO|G@+Q!3O`;RR7pu|IkAJ%Ps%YPXF0v|INcdJ{u&=}=IXLDhr+J%S1nrq(gCL;wIgri4F* literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/icon_calendar.gif b/ipf/admin/media/img/icon_calendar.gif new file mode 100644 index 0000000000000000000000000000000000000000..7587b305a4ee702cbed3bee1ae17c78feb85d00b GIT binary patch literal 192 zcmV;x06+gnNk%w1VGsZi0J8u9nVFf2iHY^~_4)bv@bK{4+uQ&D|FN{U?d|Qf&F0D5 z?Wd=w{QUgs>FMX^=l}ozA^8LW000jFEC2ui01yBW000DS@X1N*1UPGamH(iU1QH+` z43ii};vPZqm~L$+una7G@AI)4YnU1cj)Wk=I*&Aa=g_Vl48 zmH)wj0Spv>vM@3*@G|Itcpx(vSX4HgyeYC&>*nrB_bxSQsBGn6*)YRRaLr}Q6>6LJ P$Rx*~-FRR+2ZJ>L#Kbnb literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/icon_clock.gif b/ipf/admin/media/img/icon_clock.gif new file mode 100644 index 0000000000000000000000000000000000000000..ff2d57e0a3b6373b7bd9540e688b1b4c71081cb7 GIT binary patch literal 390 zcmV;10eSvMNk%w1VGsZi0M$DH{QUg>{{H#-`S$kq$lLA4+3fD_?(*{T{{H>?`uXqg z@BaV(?CkCH^Yi-p`u6wt#Mtch_4dHk>iGEh^7Hca^z*{j>crRU`1twz{QT_e?DqEc z$J*@b>gvMQ>+|&V$lC14+wAM>>;3)w@9*%!*X#51^1;{Z`}_OH+U@-O`^ehu#MkWU z>FEFe{^{xI#n|ld@$l~N@5kBgv9!0z+wHW?=G)ubr>Cd?|NohpnY8A@0000000000 z00000A^8LW0027xEC2ui01yBW000J~z@2cXD;jmfB(YW_ga{xGQmJF&uGa!=Dy-IU zx$q)@gRrJva4tXt05Uks30VcZ5F=VbfDzy%IyZGW2r3RV4+8@cI5vSg1UL%-4ihvL z4F?pBk1IFOZ1vml&CJGE4FE})gH$(*xI2#8t kA}zwiLpm2FSXaY=R2~vG+}zkoL`Ox%;6gX&=t@BVI|kg>kN^Mx literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/icon_deletelink.gif b/ipf/admin/media/img/icon_deletelink.gif new file mode 100644 index 0000000000000000000000000000000000000000..72523e3a3ba1446c8f768c157cea642119a02741 GIT binary patch literal 181 zcmZ?wbhEHbc&kkH2hg{xUB9fxq8%JG(R5 zT3?Eao`!{eDJnj1U~tLY{9|s;X>G0VWo1vp!yowhEs~V{|NlP&4xspxg^__loIwX9 z53-Yi)#yQKi;S#{{sX6 zi;Dh#`0#&e>3>Vh|D2rv=gC*q|>i-WM_<#QV|C1;G zS5*8rF!=rZ_kUyK|3JV%TcG%pg^__lo4aiRnY%vG=3p{kB40{i-Of=+@`~3X;@bK{O@9+Kn{rLF!_xJbl@$pSfP5S!!_4W1n`T6YZ?B3qqPft(K z(9rAa>(gwwA^YilZ@=HrgUS3{FNlEth_IP-BSy@@<=jZ6? z=$e|EaBy(V&CR>JyVcdz;o;$EXlUQx-&9moTU%RKS68&OwCU;T!NJ+t*-}zc&)b92AH zzpJaOZfvlTPVlRhn*D zzYj!ubf8e2}%QyRfsr*SWe%Rb`=>R1ae%6%2~5|TAxeL zzADQysO81>*ZZYCyl6@$BdV$biI?uic&0bzMrFb%Aq1#ZUv-+v+w0VD#)3jAg-{rn zQK{SBuPmhC5$}W{D84q?yXp06=7SUzUxd5@c^_-Jtgg3OcgI0-bjam_F)Z9$Uf=5< z*hz6@lo{ZZu*P|f^TenPFmQjgIBP%6a@>yq0{~})9l-cU)g%A_002ovPDHLkV1oH8 BY;phq literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/icon_success.gif b/ipf/admin/media/img/icon_success.gif new file mode 100644 index 0000000000000000000000000000000000000000..5cf90a15aa6731798b025a238fa47a1658510fff GIT binary patch literal 341 zcmZ?wbhEHb6krfwxXJ(m|NsC0@cYBBf4?q$zBut-@A?;OHoRQB_4TGbZ+4yjc=Gy} zYu7$sefa&slkbn8{dih+uDI?}MdP*V#;eu+_qwJ$=-=~p=Y>z_u719puq!NScSPpF zg#4pv1xM2>&lOf*D6TwP`0>{VkG0l*8=OKmdxUNAj@;&(aUeGLNb=vme}Ptmoj^XJ z_>+Z^fkBHw2V?}uPYi5D4pR#}bfiS5bS&9qz$LFSv4l%VM0VNAuq7M$%x`OYevyni z6y)sQCFr(n`AWsDVxcXCnHqNfPZgaKf;1I5H8q8UQ?&$wdD=UoSe-bM`a7e4!(%y#zfZ7r4Jt{2)=e(aF%jTx%D zxupe461uGCRHyBDex~(*M7c$zgJZG*%byn+ZRTGU-uBHdWY~Xu&85t+PE(B*i}F8w zNoO0s2DL<~T(x|b-mmJoS7qV4+osDF6`$U*?(}a97twW5=`D*^G`-rsR_}Lt-K!%E z`NF49zhDj(2y{8vmBi}Am_0Q>M9|{a`jl#}10}z{dke>0@i#ob-p0mG_2Z0?M=GD$ zdmF8~C6*h6zsbp0+QqlqHA&QU591UDzVD~z$h<%IN;Dz+YW(B1E2b~jVSKiy{Ppn` vD+aX#5#2a57-S&KT*C(G{DQl+NNW9y=`fHnu-v==- Ot=>b+CQo8uum%9*_Ae;_ literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/nav-bg-reverse.gif b/ipf/admin/media/img/nav-bg-reverse.gif new file mode 100644 index 0000000000000000000000000000000000000000..f11029f90fc965141b8815a78ac2651759099475 GIT binary patch literal 186 zcmV;r07d^tNk%w1VG#fy0J8u9|NsB^`1t<*{`U6v@$vEd`}^(f?dRv`;^N}{{r&3d z>hA9D(@t*9{u_A=gpfpKY#xG_U+rVXV3oq`}hC||i=Q&8(m$()z5YF*CjUEw001<4;4tjrTwH+&%FA_x~E6rskH`CdQ7= zuI>)uzWxal`(>Zw+Pr1!Chi?O LckSMx$Y2cs9wnNk literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/selector-add.gif b/ipf/admin/media/img/selector-add.gif new file mode 100644 index 0000000000000000000000000000000000000000..50132d1c439494a0a19e1d280385f03d3ac5599b GIT binary patch literal 606 zcmZ?wbhEHb6krfwcoxafw(@M}n)ChZFW!3c{`tGF)3#h)y!U$bf@7^KPS-6uzUAco zS0BDVc=>6^`De|`PR`nSyg_g{XjI&`ak@yUG` zo;EK(J#+ijhQ%lP*I%ezbo}9~&(pSE*?IQS;=MN)@4Gd3=e3%J$D5X(nz`fp(*3u$ zoqaNI*R}d3CpVpX{Q29jre&vU7apIr~bzW=sp-|cz3uWvc? zWa{QC$8Wq`dhpJqjhE{epV)Z(?&i~vr*FGDch`+I#~w`He7Swq*`){W^sT@6^~dl3 z|Nk@49Vq@}VPs&4WY7WG42ly5_T3GUO_4kje3Fi`Y|PEgg2Ei`p1iykJ|h0&z5Zgl z0rM9G$k+=tamfX*T&cAxSkXVyMZhv_>sB}KZDAbD#%4hW4zfg89~5H~^<`z%2RJ`V9$77#OFv^CvqjY0A7XSuxNk>&%VE4w{br3Jf1VK49#UQa3Uc3OT{Z NtIjEA%EiK94FJ;24lMux literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/selector-addall.gif b/ipf/admin/media/img/selector-addall.gif new file mode 100644 index 0000000000000000000000000000000000000000..d6e7c639bb2ec270122861f054289845281fae26 GIT binary patch literal 358 zcmZ?wbhEHb6krfwxN64GwdVYqV-FVYy|wM^)3z0-CvU#|?Z@x7m1n1IyE0+p#RUf* z*DpEQwCq&t%5&3q-kG-LYR}pWx1PLjUwNi;)tTmHr>AYX(!J*V^leu=SD$TPd3wsG zD~tDD-+uc3vi-Mqo_}`i`pffoUcPz%`N)+gC$B$Wu={53y7Nb`JiYSZ&EgJ2jU%oBgck}JXFNZEY-hSr6{b%p@TzI-@&-ELR-)%g8_xtzn|NsAIzzr0CvM@3* zm@(*plz{xiz}D;}e*)+h-yWNC)(RZyTq=rvah$l z-QC^j?C`?E!rI!}-rnBp>+8?Y&$F|$v%JQ(wzkK|$N2d8?d|R4>Fx3H@xZ{qud%ZB z_V&`!(&Fap*x1<3&CRj8!??J(+uPg8$;r;n&b76*)z#I=$jG<1x5UK6zrVlr`1}9= z{{R30A^8LW002G!EC2ui01yBW000K7K%dYz*=R~SmZ2~lJ~9pwN1*_@60%NHMxlAX zUK2qgWbklk^e2tm$$Y6>%tp5<1aBx~ID^{#HdGh3{RjW2`-1y|llXdIX?cBNZ+_`frSFT*Tbm`{Jn=f9xc=ztz_wV0- z|Ni~?^XIp2-MVq(#`*K-_wCzv>C&Zd-@aYHe*MaoE4y~>`t<43?%lh$ZQJ(v@#E#o zmmfcV{KtY3(LiF)@D4T0VKnCT(SA zX1)$*HBn6#jwS_7P0<=>4NmpN;)-mGISX90xU|;Ev8_pU4PxOI7UpJ&^3YJ@;Za-V LdE}_8BZD;nhnJNG literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/selector-search.gif b/ipf/admin/media/img/selector-search.gif new file mode 100644 index 0000000000000000000000000000000000000000..6d5f4c74923af2daba13baa484364380fb1614b9 GIT binary patch literal 552 zcmZ?wbhEHb6krfwc*ekR;r}fg8=Jp>|9X0Qe){yu!NI}X+uO&-$I8koH8s`3!otqZ z?(W^YSy@?TW@e{PpT2zg^6J&ABO)T)-Q5!s60Tjl=Ire3>gqaW%9PsL+PQP*Ha9mX zCnrZoN9W|^#K*^f{rYv@ym@clym|8E$=&V_@LPJBVtE(?uxDXW;RaaLxbLLEYd;7Mww%N01*VNRUKY#wgg9n+J znUg0^j*E-y=;*j{51X2rQc_ZG-@bkS{(VPBM~0yT6o0ZXf?TTuB0zD%z~0)x z(A3=0+M*~cF2*k1)@;Hotjo#FYS$;lCc@9cB5K1e*(T4%&$OJ0N1L&YotI^~f{vr% zn$~qnssdI5qW0Tb4Ak_r)E!)0jSnm~~hECZvY5o%BVqvfb0K*)eZU6uP literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/selector_stacked-add.gif b/ipf/admin/media/img/selector_stacked-add.gif new file mode 100644 index 0000000000000000000000000000000000000000..74261696522de5819780082d9d92d75f8ffdb4fc GIT binary patch literal 612 zcmZ?wbhEHb6krfwcoxdgw({()C+`>Uz23RzeCvwSk6wMQUT~~#(eW)O@4x%}qhZO( z=4B^W9lAAn)1{a1zimDJ=;PO)eQVFX`tbe1%TKd*UVs1P$KQYdUVr@FvFhyR6ZdwU ze>P#`rA;RvPThQ^Vacg^yRJ>!dS%hR+gr{&*>?76|N09vw_ojAb8ha=YxRpy&e(CS zX5q0dryguM`QY`3Zw-r2%-nIkdHLz4Wv6Nu9$&Km_M1=N`!`%%e(?6hjhE^cpICbE zPW_UT?JLi0J9WQr-Gy6EKkPjFXz{*VlQv#%T6(H>;qm&#Ct6mV*>vi0?V{s*E<9;p zb#~+NyGsw;*?ju(++8>N)?b{q|9^%71&Tje7#SEs z8FWCd0L2Ld`@x3LrchxXZVfp-CdTGwF-aChB`GOoO9?O0UN2rZpZN=XRJAypI8=gG zu3D|&6>7p|?7dlrpI>J4W)?;pTi^Y@;vpg82Yh*%1Red)_zN8tI_odX#vXkAI^SWw z8`quLnIE#U3LF+-wUd@+HWGAYV9+`IIYj*f0~?2-=O67a@_!~B5V-KDV{)@!9S?KU zg-$*<4i1Nz9ITzZT>%EXL7SAjj`z#?N@%FQnAjwxsS!|7=*eL;*Vos_$H(O9?cUzr!^6YN%ge8^vckf`>gwvV zyvDP$v)I_!vAV;~&d%H0+sMes)z#I>$;r33x4*x?xVX5rwY9{=#P#_5|Ns9000000 z00000A^8LW002AyEC2ui01yBW000KAK%S5naXdmc6(KMjYAO&AVzD@V4lqm;hLU-| z-jabLRNQE28e){D>0mrlZWF7McDqHrjtjIdGl3s?cnEyf&#j1HCpm1R^ds$Tu?y v2{Xwz1xyMMI@Q+KSVJ%eHQnCb7h_HpIV%Yq94R_BSxrJk53meNO+f%VqdU!B literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/tool-left.gif b/ipf/admin/media/img/tool-left.gif new file mode 100644 index 0000000000000000000000000000000000000000..011490ff3a0100bea63eca7d8a3f821edecf6d3f GIT binary patch literal 197 zcmV;$06PCiNk%w1VF>^d0K@+9+1 z>Fn(6>gwvFqods1+`_`bsi~>N#Kh<4=efDLot>Sks;aTEvEJU^(b3W4lL@fC0syEMTS%hy>Zm!0PcpEpJwG;hh<#5^Y_P4;QTzJF~^?;ZNVo;-~tj z3GxIURaQ?9NK-h~-O_%Ehr|6W>p_W03|&Y3moVBW-4OX{v_eKqd6L;=PDTc60DL-S AivR!s literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/tool-right.gif b/ipf/admin/media/img/tool-right.gif new file mode 100644 index 0000000000000000000000000000000000000000..cdc140cc590a56bf45ceef6eaeebf47e4a699ac3 GIT binary patch literal 198 zcmV;%06G6hNk%w1VF>^d0K@+9+1 z>Fn(6>gwvFqods1+`_`bsi~>N#Kh<4=efDLot>Sks;aTEvEJU^(b3W4R literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/tool-right_over.gif b/ipf/admin/media/img/tool-right_over.gif new file mode 100644 index 0000000000000000000000000000000000000000..4db977e838dd97ae4f59524a764cf8298f19ccc5 GIT binary patch literal 200 zcmZ?wbhEHblL@fC0syEMTS%hy>Zm!0M`SJhE@w+}Rz8zHMDu50@FVv!<=}o0a$2j_;|E wNJ^Lce8rW`28j}!f$kDtvUrX>)$-C{u;f$x#4um1{ZvBtsg!MMAsh_W0PS95AOHXW literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/tooltag-add.gif b/ipf/admin/media/img/tooltag-add.gif new file mode 100644 index 0000000000000000000000000000000000000000..8b53d49ae58dbc324ca7fb318198b187fc124c09 GIT binary patch literal 932 zcmZ?wbhEHb6lM@)_|Cxa`}glZfBv+zw0!^m{n4XG%aU|5d-nPB=MNq{xOeZ~wr$(``}?0g zdp3Xm{Fg6Zwzjt3y?giHzkgr9e*ORde?vn9!zdUHfx#C7ia%Mvj?w{lD0p`BHNuZH8`gv5jGtilW#8Ul-s@(Rm3u`n_%YH1afFWM53xbWyi0reSoZU`=F zW>B;|HRonfOG771<|UO1;}pj!{KgMbG6N3=Of=;-YN#}Nd4NeFEUKo%!I@!YFuRjN h0z*m|qaatng9}?1vhrtKUbNTY0V7A3A`=sXH2_xRgyR4J literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/tooltag-add_over.gif b/ipf/admin/media/img/tooltag-add_over.gif new file mode 100644 index 0000000000000000000000000000000000000000..bfc52f10de75998687154585752513a27a02e5c4 GIT binary patch literal 336 zcmZ?wbhEHb6lM@)xXQp_AKmol&!20zAN~IQ+b5;fJ+bBa>km(!zx7XVkIL_<>s=I* z-Fg4<%i9m06*kQ}fA#LqpFhjm=B8Ioc1vhpx_)m~&D4mz?loHv1!s0R#56sA{$|&q zv$L0NTfOzr<~=7olUmZNro(_t( z{<*|8FIl(u()IiQ|Npm*YGNP-DE?#tE7t*$AU`p%OuD z64&(xJYQ3LjTSFY75~+{EbMWTq?Tj+^`lM+ch;D9HZb0eJO28D|1uloD@pFe*-apJ^Hn>L+2d-m?#yFEQU|Ni~^ z`t|Go|Nk2r8W_k0ia%MvT6I7q$WIJxH4gmQ9x7=Hf&p1aV>r7mD6wA%agA(z(Y9-o zn{Z!7=F3()sh$@ap>8_^S=AZ45`|}1urBhDn)0zi%$1R$zCpXHxz#1VpeR78J1{*n zKx$GzLbz~Ucw``(R#|u;i#+q?x}l#o5$^*7-1rs_)vpOO{(vYlW{PgEasf C+m{mn literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/tooltag-arrowright_over.gif b/ipf/admin/media/img/tooltag-arrowright_over.gif new file mode 100644 index 0000000000000000000000000000000000000000..7163189604a638ee170f093cd042075a7da48c7a GIT binary patch literal 354 zcmZ?wbhEHb6lM@)xXQp_AKmol&!20zAN~IQ+b5;fJ+bBa>km(!zx7XVkIL_<>s=I* z-Fg4<%i9m06*kQ}fA#LqpFhjm=B8Ioc1vhpx_)m~&D4mz?loHv1!s0R#56sA{$|&q zv$L0NTfOzr<~=7olUmZNro(_t( z{<*|8FIl(u()IiQ|Npm*YGNP-DE?#tE7t*$AU`p%)jRO!c&Mb=iC%tqw1%_mf>OuD z64%5AjkaH#++@37WWH>5lj_LG3QgM?$)(Pyl_<=6_Sxb99hOrSl|Hhr49pEp`qi!N zF8KvT0m8k3>6rnFQvwpgW$MEt13C4|!UI|47?&<{5fc~Cj$Ol`%&8*1!G}pzckjNv NA{t6st9%_9tO2syk|zKF literal 0 HcmV?d00001 diff --git a/ipf/admin/media/img/topbg.gif b/ipf/admin/media/img/topbg.gif new file mode 100644 index 0000000000000000000000000000000000000000..025fcb976d945f577ca1d6702bc0332d9cef5310 GIT binary patch literal 167 zcmZ?wbhEHbWMr^sIKsfNb?epx2M(M(dGg7VCnrvvICbjOnKNg$Z{L38$dL;dE*w65 zc*~Y88#Zh>d-m+{z&!U;!Nv39^%cRpr5g-jvLFk4;oXu1d5zSaf`1nWma3Rl?Bi_4Lq=l)e)l JZQ@J})&O~aP`LmA literal 0 HcmV?d00001 diff --git a/ipf/admin/model.php b/ipf/admin/model.php new file mode 100644 index 0000000..ad6dc2a --- /dev/null +++ b/ipf/admin/model.php @@ -0,0 +1,194 @@ +modelName = $modelName; + } + + protected function _setupEditForm(&$form){ + $this->_setupForm(&$form); + } + + protected function _setupAddForm(&$form){ + $this->_setupForm(&$form); + } + + protected function _setupForm(&$form){ + } + + public function fields(){ + return null; + } + + public function ListItemsHeader(){ + $this->header = array(); + if (method_exists($this,'list_display')) + $this->names = $this->list_display(); + else + $this->names = $this->qe->getTable()->getColumnNames(); + + foreach ($this->names as $name){ + $this->header[$name] = new IPF_Template_ContextVars(array( + 'title'=>IPF_Utils::humanTitle($name), + 'name'=>$name, + 'sortable'=>null, + )); + } + return $this->header; + } + + public function ListItemsQuery(){ + $query = IPF_ORM_Query::create(); + $this->q = $query->select("o.*")->from($this->modelName.' o'); + } + + public function ListRow($o){ + $row = array(); + + foreach($this->header as &$h){ + $t = $o->getTable()->getTypeOf($h['name']); + $str = $o->$h['name']; + if ($t=='boolean'){ + if ($str) + $str = 'True'; + else + $str = 'True'; + } + $row[$h['name']] = $str; + } + $this->LinksRow(&$row, &$o); + + + return $row; + } + + protected function LinksRow(&$row, &$o){ + if (method_exists($this,'list_display_links')){ + $links_display = $this->list_display_links(); + }else{ + $links_display = null; + $i = 1; + } + foreach($row as $name=>&$v){ + if ($links_display){ + if (array_search($name, $links_display)!==false) + $v = ''.$v.''; + }else{ + if ($i==1) + $v = ''.$v.''; + $i++; + } + } + } + + protected function UrlForResult(&$o){ + return $o->__get($this->qe->getTable()->getIdentifier()).'/'; + } + + // Views Function + public function AddItem($request, $lapp, $lmodel){ + $model = new $this->modelName(); + if ($request->method == 'POST'){ + $form = IPF_Shortcuts::GetFormForModel($model,$request->POST,array('user_fields'=>$this->fields())); + $this->_setupAddForm(&$form); + if ($form->isValid()) { + $item = $form->save(); + AdminLog::logAction($request, $item, AdminLog::ADDITION); + $url = IPF_HTTP_URL_urlForView('IPF_Admin_Views_ListItems', array($lapp, $lmodel)); + return new IPF_HTTP_Response_Redirect($url); + } + } + else{ + $form = IPF_Shortcuts::GetFormForModel($model,null,array('user_fields'=>$this->fields())); + $this->_setupAddForm(&$form); + + } + $context = array( + 'page_title'=>'Add '.$this->modelName, + 'classname'=>$this->modelName, + 'form'=>$form, + 'lapp'=>$lapp, + 'lmodel'=>$lmodel, + ); + return IPF_Shortcuts::RenderToResponse('admin/add.html', $context, $request); + } + + public function EditItem($request, $lapp, $lmodel, $o){ + if ($request->method == 'POST'){ + $form = IPF_Shortcuts::GetFormForModel($o,$request->POST,array('user_fields'=>$this->fields())); + $this->_setupEditForm(&$form); + if ($form->isValid()) { + $item = $form->save(); + AdminLog::logAction($request, $item, AdminLog::CHANGE); + $url = IPF_HTTP_URL_urlForView('IPF_Admin_Views_ListItems', array($lapp, $lmodel)); + return new IPF_HTTP_Response_Redirect($url); + } + } + else{ + $form = IPF_Shortcuts::GetFormForModel($o,$o->getData(),array('user_fields'=>$this->fields())); + $this->_setupEditForm(&$form); + } + $context = array( + 'page_title'=>'Edit '.$this->modelName, + 'classname'=>$this->modelName, + 'object'=>$o, + 'form'=>$form, + 'lapp'=>$lapp, + 'lmodel'=>$lmodel, + ); + return IPF_Shortcuts::RenderToResponse('admin/change.html', $context, $request); + } + + public function DeleteItem($request, $lapp, $lmodel, $o){ + if ($request->method == 'POST'){ + AdminLog::logAction($request, $o, AdminLog::DELETION); + $o->delete(); + $url = IPF_HTTP_URL_urlForView('IPF_Admin_Views_ListItems', array($lapp, $lmodel)); + return new IPF_HTTP_Response_Redirect($url); + } + $context = array( + 'page_title'=>'Delete '.$this->modelName, + 'classname'=>$this->modelName, + 'object'=>$o, + 'form'=>$form, + 'lapp'=>$lapp, + 'lmodel'=>$lmodel, + 'affected'=>array(), + ); + return IPF_Shortcuts::RenderToResponse('admin/delete.html', $context, $request); + } + + public function ListItems($request){ + $this->ListItemsQuery(); + $this->qe = $this->q->execute(); + $this->ListItemsHeader(); + //print_r($this->qe->getTable()->getIdentifier()); + $context = array( + 'page_title'=>$this->modelName.' List', + 'header'=>$this->header, + 'classname'=>$this->modelName, + 'objects'=>$this->qe, + 'classname'=>$this->modelName, + ); + return IPF_Shortcuts::RenderToResponse('admin/items.html', $context, $request); + } +} diff --git a/ipf/admin/models.yml b/ipf/admin/models.yml new file mode 100644 index 0000000..0e27a3e --- /dev/null +++ b/ipf/admin/models.yml @@ -0,0 +1,29 @@ +AdminLog: + tableName: admin_log + actAs: + Timestampable: + updated: + disabled: true + columns: + username: string(32) + user_id: integer + object_id: integer + object_class: string(200) + object_repr: string(200) + action_flag: integer + change_message: string(200) + + indexes: + idx_object_id: + fields: object_id + idx_object_class: + fields: object_class + idx_created_at: + fields: created_at + idx_action_flag: + fields: action_flag + + options: + type: INNODB + collate: utf8_unicode_ci + charset: utf8 diff --git a/ipf/admin/models/AdminLog.php b/ipf/admin/models/AdminLog.php new file mode 100644 index 0000000..f84c33d --- /dev/null +++ b/ipf/admin/models/AdminLog.php @@ -0,0 +1,44 @@ +username = $request->user->username; + $log->user_id = $request->user->id; + $log->object_id = $object->id; + $log->object_class = get_class($object); + $log->object_repr = (string)$object; + $log->action_flag = $action_flag; + $log->change_message = $message; + $log->save(); + } + + public function is_addition(){ + if ($this->action_flag==AdminLog::ADDITION) + return true; + return false; + } + + public function is_change(){ + if ($this->action_flag==AdminLog::CHANGE) + return true; + return false; + } + + public function is_deletion(){ + if ($this->action_flag==AdminLog::DELETION) + return true; + return false; + } + + public function GetAdminUrl(){ + return IPF_HTTP_URL_urlForView('IPF_Admin_Views_Index').IPF_Utils::appLabelByModel($this->object_class).'/'.strtolower($this->object_class).'/'.$this->object_id.'/'; + } + +} + diff --git a/ipf/admin/models/_generated/BaseAdminLog.php b/ipf/admin/models/_generated/BaseAdminLog.php new file mode 100644 index 0000000..e7114fc --- /dev/null +++ b/ipf/admin/models/_generated/BaseAdminLog.php @@ -0,0 +1,34 @@ +setTableName('admin_log'); + $this->hasColumn('username', 'string', 32, array('type' => 'string', 'length' => '32')); + $this->hasColumn('user_id', 'integer', null, array('type' => 'integer')); + $this->hasColumn('object_id', 'integer', null, array('type' => 'integer')); + $this->hasColumn('object_class', 'string', 200, array('type' => 'string', 'length' => '200')); + $this->hasColumn('object_repr', 'string', 200, array('type' => 'string', 'length' => '200')); + $this->hasColumn('action_flag', 'integer', null, array('type' => 'integer')); + $this->hasColumn('change_message', 'string', 200, array('type' => 'string', 'length' => '200')); + + + $this->index('idx_object_id', array('fields' => 'object_id')); + $this->index('idx_object_class', array('fields' => 'object_class')); + $this->index('idx_created_at', array('fields' => 'created_at')); + $this->index('idx_action_flag', array('fields' => 'action_flag')); + $this->option('type', 'INNODB'); + $this->option('collate', 'utf8_unicode_ci'); + $this->option('charset', 'utf8'); + } + + public function setUp() + { + $timestampable0 = new IPF_ORM_Template_Timestampable(array('updated' => array('disabled' => true))); + $this->actAs($timestampable0); + } +} \ No newline at end of file diff --git a/ipf/admin/templates/admin/add.html b/ipf/admin/templates/admin/add.html new file mode 100644 index 0000000..66dc26b --- /dev/null +++ b/ipf/admin/templates/admin/add.html @@ -0,0 +1,25 @@ +{extends "admin/base.html"} + +{block breadcrumbs} Home » {$classname} » {$page_title}{/block} + +{block content} + +
+

{$page_title}

+ +
+
+ + {$form.render_table} +
+
+ +
+ +
+
+ +
+ +{/block} + diff --git a/ipf/admin/templates/admin/base-simple.html b/ipf/admin/templates/admin/base-simple.html new file mode 100644 index 0000000..73b82b3 --- /dev/null +++ b/ipf/admin/templates/admin/base-simple.html @@ -0,0 +1,16 @@ + + + + + + {block style}{/block} + {block head}{/block} + {$page_title} + + + + +{block body}{/block} + + + diff --git a/ipf/admin/templates/admin/base.html b/ipf/admin/templates/admin/base.html new file mode 100644 index 0000000..cff3465 --- /dev/null +++ b/ipf/admin/templates/admin/base.html @@ -0,0 +1,34 @@ + + + + + + {$page_title} - IPF Administration + + + + + +
+ +
+
+

{trans 'IPF Administration'}

+
+
Welcome, {$user}. Log out
Version: {$IPF_VER}
+
+
+ + + + + {block content}{/block} + +
+
+ +{block scripts}{/block} + + diff --git a/ipf/admin/templates/admin/change.html b/ipf/admin/templates/admin/change.html new file mode 100644 index 0000000..0cc037e --- /dev/null +++ b/ipf/admin/templates/admin/change.html @@ -0,0 +1,27 @@ +{extends "admin/base.html"} + +{block breadcrumbs} Home » {$classname} » {$page_title}{/block} + + +{block content} + +
+

{$page_title}

+ +
+
+ + {$form.render_table} +
+
+ +
+

Delete

+ +
+
+ +
+ +{/block} + diff --git a/ipf/admin/templates/admin/changepassword.html b/ipf/admin/templates/admin/changepassword.html new file mode 100644 index 0000000..d62e3af --- /dev/null +++ b/ipf/admin/templates/admin/changepassword.html @@ -0,0 +1,26 @@ +{extends "admin/base.html"} + +{block breadcrumbs} Home » {$classname} » {$object} » Change Password{/block} + + +{block content} + +
+

{$page_title}

+ +
+
+ + {$form.render_table} +
+
+ +
+ +
+
+ +
+ +{/block} + diff --git a/ipf/admin/templates/admin/delete.html b/ipf/admin/templates/admin/delete.html new file mode 100644 index 0000000..cf3b1ce --- /dev/null +++ b/ipf/admin/templates/admin/delete.html @@ -0,0 +1,25 @@ +{extends "admin/base.html"} + +{block breadcrumbs} Home » {$classname} » {$object} » {$page_title}{/block} + +{block content} + +
+

{$page_title}

+ +
+

Are you sure you want to delete {$object}? +

+ {if count($affected) > 0} +

It will also delete the following linked elements:

+
    + {foreach $affected as $aff}
  • {$aff}
  • {/foreach} +
+ {/if} + +
+ +
+ +{/block} + diff --git a/ipf/admin/templates/admin/index.html b/ipf/admin/templates/admin/index.html new file mode 100644 index 0000000..b1f05a6 --- /dev/null +++ b/ipf/admin/templates/admin/index.html @@ -0,0 +1,36 @@ +{extends "admin/base.html"} + +{block content} + +
+

{$page_title}

+
+
+ {foreach $app_list as $app} +

{$app.name|escxml}

+ + {foreach $app.models as $model} + + + + + + {/foreach} +
{$model.name}{trans 'Add'}{trans 'Change'}
+ {/foreach} +
+
+ +
+
+{/block} + diff --git a/ipf/admin/templates/admin/items.html b/ipf/admin/templates/admin/items.html new file mode 100644 index 0000000..85d1670 --- /dev/null +++ b/ipf/admin/templates/admin/items.html @@ -0,0 +1,34 @@ +{extends "admin/base.html"} + +{block breadcrumbs} Home » {$classname}{/block} + + +{block content} + +
+

{$page_title}

+ +
+ + + + {foreach $header as $h} + + {/foreach} + + + + {foreach $objects as $o} + + {foreach $o.ModelAdmin().ListRow($o) as $v} + + {/foreach} + + {/foreach} + +
{$h.title}
{$v|safe}
+
+
+ +{/block} + diff --git a/ipf/admin/templates/admin/login.html b/ipf/admin/templates/admin/login.html new file mode 100644 index 0000000..40c793c --- /dev/null +++ b/ipf/admin/templates/admin/login.html @@ -0,0 +1,26 @@ +{extends 'admin/base-simple.html'} + +{block body} + +
+
+

{$page_title}

+ +
+
+ {if $form.message}
  • {$form.message}
{/if} + + {$form.render_table} +
+
+ +
+ +
+
+ +
+
+ + +{/block} diff --git a/ipf/admin/templates/admin/logout.html b/ipf/admin/templates/admin/logout.html new file mode 100644 index 0000000..7d5a9f6 --- /dev/null +++ b/ipf/admin/templates/admin/logout.html @@ -0,0 +1,16 @@ +{extends 'admin/base-simple.html'} + +{block body} + +
+
+

Logged Out

+ +

Thanks for spending some quality time with the Web site today.

+

Log in again.

+ +
+
+ + +{/block} diff --git a/ipf/admin/views.php b/ipf/admin/views.php new file mode 100644 index 0000000..971c7c8 --- /dev/null +++ b/ipf/admin/views.php @@ -0,0 +1,205 @@ +user->isAnonymous()) + return new IPF_HTTP_Response_Redirect(IPF_HTTP_URL_urlForView('IPF_Admin_Views_Login')); + return true; +} + +function IPF_Admin_Views_Index($request, $match){ + $ca = checkAdminAuth($request); + if ($ca!==true) return $ca; + + $apps = array(); + $app_list = new IPF_Template_ContextVars(); + foreach (IPF_Project::getInstance()->appList() as $app){ + if (count($app->modelList())>0){ + $models = new IPF_Template_ContextVars(); + $models_found = false; + foreach($app->modelList() as $m){ + if (IPF_Admin_Model::isModelRegister($m)){ + $models[] = new IPF_Template_ContextVars(array('name'=>$m, 'path'=>strtolower($m))); + $models_found = true; + } + } + if ($models_found){ + $app_list[$app->getName()] = new IPF_Template_ContextVars(array( + 'name' => $app->getTitle(), + 'path' => $app->getSlug(), + 'models' => $models, + )); + } + } + } + + $admin_log = IPF_ORM_Query::create()->select("*")->from('AdminLog')->orderby('created_at desc')->limit(10)->execute(); + + $context = array( + 'page_title' => __('Administration'), + 'app_list' => $app_list, + 'admin_log' => $admin_log, + ); + return IPF_Shortcuts::RenderToResponse('admin/index.html', $context, $request); +} + +function IPF_Admin_Views_ListItems($request, $match){ + $ca = checkAdminAuth($request); + if ($ca!==true) return $ca; + + $lmodel = $match[2]; + foreach (IPF_Project::getInstance()->appList() as $app){ + foreach($app->modelList() as $m){ + if (strtolower($m)==$lmodel){ + $ma = IPF_Admin_Model::getModelAdmin($m); + if ($ma===null) + return new IPF_HTTP_Response_NotFound(); + return $ma->ListItems($request); + } + } + } +} + +function IPF_Admin_Views_EditItem($request, $match){ + $ca = checkAdminAuth($request); + if ($ca!==true) return $ca; + + $lapp = $match[1]; + $lmodel = $match[2]; + $id = $match[3]; + foreach (IPF_Project::getInstance()->appList() as $app){ + foreach($app->modelList() as $m){ + if (strtolower($m)==$lmodel){ + $ma = IPF_Admin_Model::getModelAdmin($m); + if ($ma===null) + return new IPF_HTTP_Response_NotFound(); + $o = new $m(); + $item = $o->getTable()->find($id); + return $ma->EditItem($request, $lapp, $lmodel, &$item); + } + } + } +} + +function IPF_Admin_Views_DeleteItem($request, $match){ + $ca = checkAdminAuth($request); + if ($ca!==true) return $ca; + + $lapp = $match[1]; + $lmodel = $match[2]; + $id = $match[3]; + foreach (IPF_Project::getInstance()->appList() as $app){ + foreach($app->modelList() as $m){ + if (strtolower($m)==$lmodel){ + $ma = IPF_Admin_Model::getModelAdmin($m); + if ($ma===null) + return new IPF_HTTP_Response_NotFound(); + $o = new $m(); + $item = $o->getTable()->find($id); + return $ma->DeleteItem($request, $lapp, $lmodel, &$item); + } + } + } +} + + +function IPF_Admin_Views_AddItem($request, $match){ + $ca = checkAdminAuth($request); + if ($ca!==true) return $ca; + + $lapp = $match[1]; + $lmodel = $match[2]; + foreach (IPF_Project::getInstance()->appList() as $app){ + foreach($app->modelList() as $m){ + if (strtolower($m)==$lmodel){ + $ma = IPF_Admin_Model::getModelAdmin($m); + if ($ma===null) + return new IPF_HTTP_Response_NotFound(); + return $ma->AddItem($request, $lapp, $lmodel); + } + } + } +} + +function IPF_Admin_Views_ChangePassword($request, $match){ + $ca = checkAdminAuth($request); + if ($ca!==true) return $ca; + + $lapp = 'auth'; + $lmodel = 'user'; + $id = $match[1]; + foreach (IPF_Project::getInstance()->appList() as $app){ + foreach($app->modelList() as $m){ + if (strtolower($m)==$lmodel){ + $ma = IPF_Admin_Model::getModelAdmin($m); + if ($ma===null) + return new IPF_HTTP_Response_NotFound(); + $o = new $m(); + $user = $o->getTable()->find($id); + + if ($request->method == 'POST'){ + $form = new IPF_Auth_Forms_ChangePassword($request->POST); + if ($form->isValid()) { + $user->setPassword($form->cleaned_data['password1']); + $user->save(); + $url = IPF_HTTP_URL_urlForView('IPF_Admin_Views_ListItems', array($lapp, $lmodel)); + return new IPF_HTTP_Response_Redirect($url); + } + } + else + $form = new IPF_Auth_Forms_ChangePassword(); + $context = array( + 'page_title'=>'Change Password: '.$user->username, + 'classname'=>'User', + 'object'=>$user, + 'form'=>$form, + 'lapp'=>$lapp, + 'lmodel'=>$lmodel, + ); + return IPF_Shortcuts::RenderToResponse('admin/changepassword.html', $context, $request); + } + } + } +} + + +function IPF_Admin_Views_Login($request, $match){ + if (!empty($request->REQUEST['next'])) + $success_url = $request->REQUEST['next']; + if (trim($success_url)=='') + $success_url = IPF_HTTP_URL_urlForView('IPF_Admin_Views_Index'); + + if ($request->method == 'POST') { + $form = new IPF_Auth_Forms_Login($request->POST); + if ($form->isValid()){ + $users = new User(); + if (false === ($user = $users->checkCreditentials($form->cleaned_data['username'], $form->cleaned_data['password']))) { + $form->message = __('The login or the password is not valid. The login and the password are case sensitive.'); + } else { + $request->user = $user; + $request->session->clear(); + $request->session->setData('login_time', gmdate('Y-m-d H:i:s')); + $user->last_login = gmdate('Y-m-d H:i:s'); + $user->save(); + return new IPF_HTTP_Response_Redirect($success_url); + } + } + } + else + $form = new IPF_Auth_Forms_Login(array('next'=>$success_url)); + $context = array( + 'page_title' => __('IPF Administration'), + 'form' => $form, + ); + return IPF_Shortcuts::RenderToResponse('admin/login.html', $context, $request); +} + +function IPF_Admin_Views_Logout($request, $match){ + $request->user = new User(); + $request->session->clear(); + $request->session->setData('logout_time', gmdate('Y-m-d H:i:s')); + + $context = array( + 'page_title' => __('IPF Administration'), + ); + return IPF_Shortcuts::RenderToResponse('admin/logout.html', $context, $request); +} diff --git a/ipf/application.php b/ipf/application.php new file mode 100644 index 0000000..d230bd3 --- /dev/null +++ b/ipf/application.php @@ -0,0 +1,73 @@ +setName(); + if (array_key_exists('models',$data)){ + foreach($data['models'] as &$modelname){ + if (!IPF_Utils::isValidName($modelname)) + throw new IPF_Exception_Panic("Model name \"$name\" is incorrect"); + $this->models[] = $modelname; + } + } + } + + protected function setName(){ + $this->name = str_replace('_App', '', get_class($this)); + if (strpos($this->name,'IPF_')===0) + $this->path = IPF::get('ipf_path').DIRECTORY_SEPARATOR.'ipf'.DIRECTORY_SEPARATOR.strtolower(str_replace('_',DIRECTORY_SEPARATOR,str_replace('IPF_','',$this->name))); + else + $this->path = IPF::get('project_path').DIRECTORY_SEPARATOR.strtolower(str_replace('_',DIRECTORY_SEPARATOR,$this->name)); + $this->path .= DIRECTORY_SEPARATOR; + } + + public function generateSql(){ + if (count($this->models)==0) + return; + return IPF_ORM::generateSqlFromModels($this->path.'models'); + } + + public function modelList(){ + return $this->models; + } + + public function getName(){ + return $this->name; + } + + public function getLabel(){ + return str_replace('ipf_','',strtolower($this->name)); + } + + + public function getTitle(){ + return $this->name; + } + + public function getSlug(){ + $e = explode('_',$this->name); + return strtolower($e[count($e)-1]); + } + + public function createTablesFromModels(){ + if (count($this->models)==0) + return; + return IPF_ORM::createTablesFromModels($this->path.'models'); + } + + public function generateModels(){ + //if (count($this->models)==0) + // return; + IPF_ORM::generateModelsFromYaml($this->path.'models.yml', $this->path.'models'); + } + + public function loadModels(){ + if (count($this->models)==0) + return; + IPF_ORM::loadModels($this->path.'models'); + } +} \ No newline at end of file diff --git a/ipf/auth/app.php b/ipf/auth/app.php new file mode 100644 index 0000000..f1d42f5 --- /dev/null +++ b/ipf/auth/app.php @@ -0,0 +1,9 @@ +array('User','Role') + )); + } +} \ No newline at end of file diff --git a/ipf/auth/forms/changepassword.php b/ipf/auth/forms/changepassword.php new file mode 100644 index 0000000..f63af96 --- /dev/null +++ b/ipf/auth/forms/changepassword.php @@ -0,0 +1,22 @@ +fields['password1'] = new IPF_Form_Field_Varchar(array('required'=>true,'widget'=>'IPF_Form_Widget_PasswordInput')); + $this->fields['password2'] = new IPF_Form_Field_Varchar(array('required'=>true,'widget'=>'IPF_Form_Widget_PasswordInput','help_text'=>'Enter the same password as above, for verification.')); + } + + function isValid(){ + $ok = parent::isValid(); + if ($ok===true){ + if ($this->cleaned_data['password1']!=$this->cleaned_data['password2']){ + $this->is_valid = false; + $this->errors['password2'][] = "The two password fields didn't match."; + return false; + } + } + return $ok; + } +} diff --git a/ipf/auth/forms/login.php b/ipf/auth/forms/login.php new file mode 100644 index 0000000..ab55603 --- /dev/null +++ b/ipf/auth/forms/login.php @@ -0,0 +1,9 @@ +fields['username'] = new IPF_Form_Field_Varchar(array('required'=>true)); + $this->fields['password'] = new IPF_Form_Field_Varchar(array('required'=>true,'widget'=>'IPF_Form_Widget_PasswordInput')); + $this->fields['next'] = new IPF_Form_Field_Varchar(array('required'=>false,'widget'=>'IPF_Form_Widget_HiddenInput')); + } +} diff --git a/ipf/auth/forms/usercreation.php b/ipf/auth/forms/usercreation.php new file mode 100644 index 0000000..29e942b --- /dev/null +++ b/ipf/auth/forms/usercreation.php @@ -0,0 +1,38 @@ +fields['username']->help_text = 'Required. 32 characters or fewer. Alphanumeric characters only (letters, digits and underscores).'; + } + + function add__password1__field(){ + $this->fields['password1'] = new IPF_Form_Field_Varchar(array('label'=>'Password','required'=>true,'max_length'=>32,'widget'=>'IPF_Form_Widget_PasswordInput')); + } + + function add__password2__field(){ + $this->fields['password2'] = new IPF_Form_Field_Varchar(array('label'=>'Password (again)','required'=>true,'max_length'=>32,'widget'=>'IPF_Form_Widget_PasswordInput','help_text'=>'Enter the same password as above, for verification.')); + } + + function isValid(){ + $ok = parent::isValid(); + if ($ok===true){ + if ($this->cleaned_data['password1']!=$this->cleaned_data['password2']){ + $this->is_valid = false; + $this->errors['password2'][] = "The two password fields didn't match."; + return false; + } + } + return $ok; + } +} diff --git a/ipf/auth/models.yml b/ipf/auth/models.yml new file mode 100644 index 0000000..d314f83 --- /dev/null +++ b/ipf/auth/models.yml @@ -0,0 +1,127 @@ +--- +User: + tableName: auth_user + actAs: [Timestampable] + columns: + username: + type: string(32) + notblank: true + notnull: true + unique: true + password: + type: string(128) + notblank: true + notnull: true + first_name: string(32) + last_name: string(32) + email: + type: string(200) + email: true + notnull: true + notblank: true + unique: true + is_staff: + type: boolean + notnull: true + default: false + is_active: + type: boolean + notnull: true + default: false + is_superuser: + type: boolean + notnull: true + default: false + last_login: + type: timestamp + relations: + Roles: + class: Role + refClass: UserRole + foreignAlias: Users + Permissions: + class: Permission + refClass: UserPermission + foreignAlias: Users + options: + type: INNODB + collate: utf8_unicode_ci + charset: utf8 +Role: + tableName: auth_role + columns: + name: + unique: true + type: string(255) + relations: + Permissions: + class: Permissions + refClass: RolePermission + foreignAlias: Roles + options: + type: INNODB + collate: utf8_unicode_ci + charset: utf8 + + +Permission: + tableName: auth_permission + columns: + name: + unique: true + type: string(255) + options: + type: INNODB + collate: utf8_unicode_ci + charset: utf8 + +RolePermission: + tableName: auth_role_permission + columns: + role_id: + type: integer + primary: true + permission_id: + type: integer + primary: true + relations: + Role: + Permission: + options: + type: INNODB + collate: utf8_unicode_ci + charset: utf8 + +UserRole: + tableName: auth_user_role + columns: + user_id: + type: integer + primary: true + role_id: + type: integer + primary: true + relations: + User: + Role: + options: + type: INNODB + collate: utf8_unicode_ci + charset: utf8 + +UserPermission: + tableName: auth_user_permission + columns: + user_id: + type: integer + primary: true + permission_id: + type: integer + primary: true + relations: + User: + Permission: + options: + type: INNODB + collate: utf8_unicode_ci + charset: utf8 diff --git a/ipf/auth/models/Permission.php b/ipf/auth/models/Permission.php new file mode 100644 index 0000000..321c925 --- /dev/null +++ b/ipf/auth/models/Permission.php @@ -0,0 +1,9 @@ +fields['username']->help_text = 'Required. 32 characters or fewer. Alphanumeric characters only (letters, digits and underscores).'; + $form->fields['password']->help_text = "Use '[algo]$[salt]$[hexdigest]' or use the change password form."; + } + + public function AddItem($request, $lapp, $lmodel){ + $model = new $this->modelName(); + if ($request->method == 'POST'){ + $form = new IPF_Auth_Forms_UserCreation($request->POST); + if ($form->isValid()) { + $user = User::createUser( + $form->cleaned_data['username'], + $form->cleaned_data['password1'], + $form->cleaned_data['email'], + $form->cleaned_data['first_name'], + $form->cleaned_data['last_name'], + $form->cleaned_data['is_active'], + $form->cleaned_data['is_staff'], + $form->cleaned_data['is_superuser'] + ); + $url = IPF_HTTP_URL_urlForView('IPF_Admin_Views_ListItems', array($lapp, $lmodel)); + return new IPF_HTTP_Response_Redirect($url); + } + } + else + $form = new IPF_Auth_Forms_UserCreation(); + $context = array( + 'page_title'=>'Add '.$this->modelName, + 'classname'=>$this->modelName, + 'form'=>$form, + 'lapp'=>$lapp, + 'lmodel'=>$lmodel, + ); + return IPF_Shortcuts::RenderToResponse('admin/add.html', $context, $request); + } +} + + +class User extends BaseUser +{ + const UNUSABLE_PASSWORD = '!'; + public $session_key = 'IPF_User_auth'; + + public function __toString() { + $s = $this->username; + if ($s===null) + return 'Anonymous'; + return $s; + } + + static function createUser($username, $password=null, $email=null, $first_name=null, $last_name=null, $is_active=false, $is_staff=false, $is_superuser=false){ + $user = new User(); + $user->username = $username; + + if (trim($email)=='') + $user->email = null; + else + $user->email = $email; + + $user->first_name = $first_name; + $user->last_name = $last_name; + $user->is_active = $is_active; + $user->is_staff = $is_staff; + $user->is_superuser = $is_superuser; + + if ($password!==null) + $user->setPassword($password); + else + $user->setUnusablePassword(); + + try { + $user->save(); + } catch(IPF_ORM_Exception_Validator $e) { + //print_r($e); + // Note: you could also use $e->getInvalidRecords(). The direct way + // used here is just more simple when you know the records you're dealing with. + $userErrors = $user->getErrorStack(); + //$emailErrors = $user->email->getErrorStack(); + // Inspect user errors + foreach($userErrors as $fieldName => $errorCodes) { + echo "Error:".$fieldName; + print_r($errorCodes); + } + } + return $user; + } + + function setUnusablePassword(){ + $this->password = UNUSABLE_PASSWORD; + } + + function setPassword($raw_password){ + $salt = IPF_Utils::randomString(5); + $this->password = 'sha1:'.$salt.':'.sha1($salt.$raw_password); + } + + function checkPassword($password){ + if ( ($this->password=='') || ($this->password==User::UNUSABLE_PASSWORD) ) + return false; + list($algo, $salt, $hash) = split(':', $this->password); + if ($hash == $algo($salt.$password)) + return true; + else + return false; + } + + function isAnonymous() + { + if (0===(int)$this->id) + return true; + return false; + } + + function checkCreditentials($username, $password) + { + $user = $this->getTable()->findOneByUsername($username); + if ($user === false) { + return false; + } + if ($user->is_active and $user->checkPassword($password)) { + return $user; + } + return false; + } +} + +IPF_Admin_Model::register(User,AdminUser); diff --git a/ipf/auth/models/UserPermission.php b/ipf/auth/models/UserPermission.php new file mode 100644 index 0000000..58111cc --- /dev/null +++ b/ipf/auth/models/UserPermission.php @@ -0,0 +1,9 @@ +setTableName('auth_permission'); + $this->hasColumn('name', 'string', 255, array('unique' => true, 'type' => 'string', 'length' => '255')); + + $this->option('type', 'INNODB'); + $this->option('collate', 'utf8_unicode_ci'); + $this->option('charset', 'utf8'); + } + + public function setUp() + { + $this->hasMany('User as Users', array('refClass' => 'UserPermission', + 'local' => 'permission_id', + 'foreign' => 'user_id')); + + $this->hasMany('RolePermission', array('local' => 'id', + 'foreign' => 'permission_id')); + + $this->hasMany('UserPermission', array('local' => 'id', + 'foreign' => 'permission_id')); + } +} \ No newline at end of file diff --git a/ipf/auth/models/_generated/BaseRole.php b/ipf/auth/models/_generated/BaseRole.php new file mode 100644 index 0000000..6fa37d5 --- /dev/null +++ b/ipf/auth/models/_generated/BaseRole.php @@ -0,0 +1,30 @@ +setTableName('auth_role'); + $this->hasColumn('name', 'string', 255, array('unique' => true, 'type' => 'string', 'length' => '255')); + + $this->option('type', 'INNODB'); + $this->option('collate', 'utf8_unicode_ci'); + $this->option('charset', 'utf8'); + } + + public function setUp() + { + $this->hasMany('User as Users', array('refClass' => 'UserRole', + 'local' => 'role_id', + 'foreign' => 'user_id')); + + $this->hasMany('RolePermission', array('local' => 'id', + 'foreign' => 'role_id')); + + $this->hasMany('UserRole', array('local' => 'id', + 'foreign' => 'role_id')); + } +} \ No newline at end of file diff --git a/ipf/auth/models/_generated/BaseRolePermission.php b/ipf/auth/models/_generated/BaseRolePermission.php new file mode 100644 index 0000000..6a499b7 --- /dev/null +++ b/ipf/auth/models/_generated/BaseRolePermission.php @@ -0,0 +1,27 @@ +setTableName('auth_role_permission'); + $this->hasColumn('role_id', 'integer', null, array('type' => 'integer', 'primary' => true)); + $this->hasColumn('permission_id', 'integer', null, array('type' => 'integer', 'primary' => true)); + + $this->option('type', 'INNODB'); + $this->option('collate', 'utf8_unicode_ci'); + $this->option('charset', 'utf8'); + } + + public function setUp() + { + $this->hasOne('Role', array('local' => 'role_id', + 'foreign' => 'id')); + + $this->hasOne('Permission', array('local' => 'permission_id', + 'foreign' => 'id')); + } +} \ No newline at end of file diff --git a/ipf/auth/models/_generated/BaseUser.php b/ipf/auth/models/_generated/BaseUser.php new file mode 100644 index 0000000..1027aab --- /dev/null +++ b/ipf/auth/models/_generated/BaseUser.php @@ -0,0 +1,45 @@ +setTableName('auth_user'); + $this->hasColumn('username', 'string', 32, array('type' => 'string', 'notblank' => true, 'notnull' => true, 'unique' => true, 'length' => '32')); + $this->hasColumn('password', 'string', 128, array('type' => 'string', 'notblank' => true, 'notnull' => true, 'length' => '128')); + $this->hasColumn('first_name', 'string', 32, array('type' => 'string', 'length' => '32')); + $this->hasColumn('last_name', 'string', 32, array('type' => 'string', 'length' => '32')); + $this->hasColumn('email', 'string', 200, array('type' => 'string', 'email' => true, 'notnull' => true, 'notblank' => true, 'unique' => true, 'length' => '200')); + $this->hasColumn('is_staff', 'boolean', null, array('type' => 'boolean', 'notnull' => true, 'default' => false)); + $this->hasColumn('is_active', 'boolean', null, array('type' => 'boolean', 'notnull' => true, 'default' => false)); + $this->hasColumn('is_superuser', 'boolean', null, array('type' => 'boolean', 'notnull' => true, 'default' => false)); + $this->hasColumn('last_login', 'timestamp', null, array('type' => 'timestamp')); + + $this->option('type', 'INNODB'); + $this->option('collate', 'utf8_unicode_ci'); + $this->option('charset', 'utf8'); + } + + public function setUp() + { + $this->hasMany('Role as Roles', array('refClass' => 'UserRole', + 'local' => 'user_id', + 'foreign' => 'role_id')); + + $this->hasMany('Permission as Permissions', array('refClass' => 'UserPermission', + 'local' => 'user_id', + 'foreign' => 'permission_id')); + + $this->hasMany('UserRole', array('local' => 'id', + 'foreign' => 'user_id')); + + $this->hasMany('UserPermission', array('local' => 'id', + 'foreign' => 'user_id')); + + $timestampable0 = new IPF_ORM_Template_Timestampable(); + $this->actAs($timestampable0); + } +} \ No newline at end of file diff --git a/ipf/auth/models/_generated/BaseUserPermission.php b/ipf/auth/models/_generated/BaseUserPermission.php new file mode 100644 index 0000000..8ffd722 --- /dev/null +++ b/ipf/auth/models/_generated/BaseUserPermission.php @@ -0,0 +1,27 @@ +setTableName('auth_user_permission'); + $this->hasColumn('user_id', 'integer', null, array('type' => 'integer', 'primary' => true)); + $this->hasColumn('permission_id', 'integer', null, array('type' => 'integer', 'primary' => true)); + + $this->option('type', 'INNODB'); + $this->option('collate', 'utf8_unicode_ci'); + $this->option('charset', 'utf8'); + } + + public function setUp() + { + $this->hasOne('User', array('local' => 'user_id', + 'foreign' => 'id')); + + $this->hasOne('Permission', array('local' => 'permission_id', + 'foreign' => 'id')); + } +} \ No newline at end of file diff --git a/ipf/auth/models/_generated/BaseUserRole.php b/ipf/auth/models/_generated/BaseUserRole.php new file mode 100644 index 0000000..64881bb --- /dev/null +++ b/ipf/auth/models/_generated/BaseUserRole.php @@ -0,0 +1,27 @@ +setTableName('auth_user_role'); + $this->hasColumn('user_id', 'integer', null, array('type' => 'integer', 'primary' => true)); + $this->hasColumn('role_id', 'integer', null, array('type' => 'integer', 'primary' => true)); + + $this->option('type', 'INNODB'); + $this->option('collate', 'utf8_unicode_ci'); + $this->option('charset', 'utf8'); + } + + public function setUp() + { + $this->hasOne('User', array('local' => 'user_id', + 'foreign' => 'id')); + + $this->hasOne('Role', array('local' => 'role_id', + 'foreign' => 'id')); + } +} \ No newline at end of file diff --git a/ipf/cli.php b/ipf/cli.php new file mode 100644 index 0000000..7e881c0 --- /dev/null +++ b/ipf/cli.php @@ -0,0 +1,65 @@ +commands = array('help','sql','buildmodels','syncdb'); + } + + protected function usage(&$args){ + print "Type 'php index.php help' for usage.\n"; + } + + protected function main_help(&$args){ + print "php index.php [options] [args]\n"; + print "Available subcommands:\n"; + foreach ($this->commands as &$command) + print " $command\n"; + } + + protected function help(&$args){ + if (count($args)==2) + $this->main_help(&$args); + } + + protected function sql(&$args){ + print "Show All Sql DDL From Model Classes\n"; + print IPF_Project::getInstance()->generateSql(); + } + + protected function syncdb(&$args){ + print "Create Tables From Model Classes\n"; + IPF_Project::getInstance()->createTablesFromModels(); + } + + protected function buildmodels(&$args){ + print "Build All Model Classses\n"; + IPF_Project::getInstance()->generateModels(); + } + + public function run(){ + + print "IPF command line tool. Version: ".IPF_Version::$name."\n"; + print "Project config: ".IPF::get('settings_file')."\n"; + + $opt = new IPF_Getopt(); + //$z = $opt->getopt2($opt->readPHPArgv(), array('s',)); //, array('s',)); + $args = $opt->readPHPArgv(); + if (count($args)==1) + { + $this->usage(&$args); + return; + } + + if (in_array($args[1],$this->commands)) + { + eval('$this->'.$args[1].'(&$args);'); + return; + } + + print "Unknown command: '".$args[1]."'\n"; + $this->usage(&$args); + } +} \ No newline at end of file diff --git a/ipf/context.php b/ipf/context.php new file mode 100644 index 0000000..ad86528 --- /dev/null +++ b/ipf/context.php @@ -0,0 +1,21 @@ + $request->user); +} + +function IPF_Context_Media($request) +{ + return array('MEDIA_URL' => IPF::get('media_url')); +} + +function IPF_Context_AdminMedia($request) +{ + return array('ADMIN_MEDIA_URL' => IPF::get('admin_media_url')); +} + +function IPF_Context_Version($request) +{ + return array('IPF_VER' => IPF_Version::$name); +} diff --git a/ipf/enum.php b/ipf/enum.php new file mode 100644 index 0000000..2c3f63b --- /dev/null +++ b/ipf/enum.php @@ -0,0 +1,38 @@ +current_val = constant( "{$class_name}::{$type}" ); + } + + final public function __toString() { + return $this->current_val; + } +} diff --git a/ipf/exception.php b/ipf/exception.php new file mode 100644 index 0000000..c418db8 --- /dev/null +++ b/ipf/exception.php @@ -0,0 +1,3 @@ +data = $data; + $this->is_bound = true; + } + if ($label_suffix !== null) $this->label_suffix = $label_suffix; + + $this->initFields($extra); + $this->f = new IPF_Form_FieldProxy($this); + } + + function initFields($extra=array()) + { + throw new IPF_Exception('Definition of the fields not implemented.'); + } + + function addPrefix($field_name) + { + if ('' !== $this->prefix) { + return $this->prefix.'-'.$field_name; + } + return $field_name; + } + + function isValid() + { + if ($this->is_valid !== null) { + return $this->is_valid; + } + $this->cleaned_data = array(); + $this->errors = array(); + $form_methods = get_class_methods($this); + foreach ($this->fields as $name=>$field) { + $value = $field->widget->valueFromFormData($this->addPrefix($name), + $this->data); + try { + $value = $field->clean($value); + $this->cleaned_data[$name] = $value; + if (in_array('clean_'.$name, $form_methods)) { + $m = 'clean_'.$name; + $value = $this->$m(); + $this->cleaned_data[$name] = $value; + } + } catch (IPF_Exception_Form $e) { + if (!isset($this->errors[$name])) $this->errors[$name] = array(); + $this->errors[$name][] = $e->getMessage(); + if (isset($this->cleaned_data[$name])) { + unset($this->cleaned_data[$name]); + } + } + } + try { + $this->cleaned_data = $this->clean(); + } catch (IPF_Exception_Form $e) { + if (!isset($this->errors['__all__'])) $this->errors['__all__'] = array(); + $this->errors['__all__'][] = $e->getMessage(); + } + if (empty($this->errors)) { + $this->is_valid = true; + return true; + } + // as some errors, we do not have cleaned data available. + $this->cleaned_data = array(); + $this->is_valid = false; + return false; + } + + public function clean() + { + return $this->cleaned_data; + } + + public function initial($name) + { + if (isset($this->fields[$name])) { + return $this->fields[$name]->initial; + } + return ''; + } + + public function render_top_errors() + { + $top_errors = (isset($this->errors['__all__'])) ? $this->errors['__all__'] : array(); + array_walk($top_errors, 'IPF_Form_htmlspecialcharsArray'); + return new IPF_Template_SafeString(IPF_Form_renderErrorsAsHTML($top_errors), true); + } + + public function get_top_errors() + { + return (isset($this->errors['__all__'])) ? $this->errors['__all__'] : array(); + } + + protected function htmlOutput($normal_row, $error_row, $row_ender, + $help_text_html, $errors_on_separate_row) + { + $top_errors = (isset($this->errors['__all__'])) ? $this->errors['__all__'] : array(); + array_walk($top_errors, 'IPF_Form_htmlspecialcharsArray'); + $output = array(); + $hidden_fields = array(); + foreach ($this->fields as $name=>$field) { + $bf = new IPF_Form_BoundField($this, $field, $name); + $bf_errors = $bf->errors; + array_walk($bf_errors, 'IPF_Form_htmlspecialcharsArray'); + if ($field->widget->is_hidden) { + foreach ($bf_errors as $_e) { + $top_errors[] = sprintf(__('(Hidden field %1$s) %2$s'), + $name, $_e); + } + $hidden_fields[] = $bf; // Not rendered + } else { + if ($errors_on_separate_row and count($bf_errors)) { + $output[] = sprintf($error_row, IPF_Form_renderErrorsAsHTML($bf_errors)); + } + if (strlen($bf->label) > 0) { + $label = htmlspecialchars($bf->label, ENT_COMPAT, 'UTF-8'); + if ($this->label_suffix) { + if (!in_array(mb_substr($label, -1, 1), + array(':','?','.','!'))) { + $label .= $this->label_suffix; + } + } + if ($field->required) + $label_attrs = array('class'=>'required'); + else + $label_attrs = array(); + $label = $bf->labelTag($label,$label_attrs); + } else { + $label = ''; + } + if ($bf->help_text) { + // $bf->help_text can contains HTML and is not + // escaped. + $help_text = sprintf($help_text_html, $bf->help_text); + } else { + $help_text = ''; + } + $errors = ''; + if (!$errors_on_separate_row and count($bf_errors)) { + $errors = IPF_Form_renderErrorsAsHTML($bf_errors); + } + $output[] = sprintf($normal_row, $errors, $label, + $bf->render_w(), $help_text); + } + } + if (count($top_errors)) { + $errors = sprintf($error_row, + IPF_Form_renderErrorsAsHTML($top_errors)); + array_unshift($output, $errors); + } + if (count($hidden_fields)) { + $_tmp = ''; + foreach ($hidden_fields as $hd) { + $_tmp .= $hd->render_w(); + } + if (count($output)) { + $last_row = array_pop($output); + $last_row = substr($last_row, 0, -strlen($row_ender)).$_tmp + .$row_ender; + $output[] = $last_row; + } else { + $output[] = $_tmp; + } + } + return new IPF_Template_SafeString(implode("\n", $output), true); + } + + public function render_p() + { + return $this->htmlOutput('

%1$s%2$s %3$s%4$s

', '%s', '

', + ' %s', true); + } + + public function render_ul() + { + return $this->htmlOutput('
  • %1$s%2$s %3$s%4$s
  • ', '
  • %s
  • ', + '', ' %s', false); + } + + public function render_table() + { + return $this->htmlOutput('%2$s%1$s%3$s%4$s', + '%s', + '', '
    %s', false); + } + + function __get($prop) + { + if (!in_array($prop, array('render_p', 'render_ul', 'render_table', 'render_top_errors', 'get_top_errors'))) { + return $this->$prop; + } + return $this->$prop(); + } + + public function field($key) + { + return new IPF_Form_BoundField($this, $this->fields[$key], $key); + + } + + public function current() + { + $field = current($this->fields); + $name = key($this->fields); + return new IPF_Form_BoundField($this, $field, $name); + } + + public function key() + { + return key($this->fields); + } + + public function next() + { + next($this->fields); + } + + public function rewind() + { + reset($this->fields); + } + + public function valid() + { + return (false !== current($this->fields)); + } + +} + +function IPF_Form_htmlspecialcharsArray(&$item, $key) +{ + $item = htmlspecialchars($item, ENT_COMPAT, 'UTF-8'); +} + +function IPF_Form_renderErrorsAsHTML($errors) +{ + $tmp = array(); + foreach ($errors as $err) { + $tmp[] = '
  • '.$err.'
  • '; + } + return '
      '.implode("\n", $tmp).'
    '; +} diff --git a/ipf/form/boundfield.php b/ipf/form/boundfield.php new file mode 100644 index 0000000..bc97152 --- /dev/null +++ b/ipf/form/boundfield.php @@ -0,0 +1,101 @@ +form = $form; + $this->field = $field; + $this->name = $name; + $this->html_name = $this->form->addPrefix($name); + if ($this->field->label == '') { + $this->label = mb_ereg_replace('/\_/', '/ /', mb_ucfirst($name)); + } else { + $this->label = $this->field->label; + } + $this->help_text = ($this->field->help_text) ? $this->field->help_text : ''; + if (isset($this->form->errors[$name])) { + $this->errors = $this->form->errors[$name]; + } + } + + public function render_w($widget=null, $attrs=array()) + { + if ($widget === null) { + $widget = $this->field->widget; + } + $id = $this->autoId(); + if ($id and !array_key_exists('id', $attrs) + and !array_key_exists('id', $widget->attrs)) { + $attrs['id'] = $id; + } + if (!$this->form->is_bound) { + $data = $this->form->initial($this->name); + } else { + $data = $this->field->widget->valueFromFormData($this->html_name, $this->form->data); + } + return $widget->render($this->html_name, $data, $attrs); + } + + public function labelTag($contents=null, $attrs=array()) + { + $contents = ($contents) ? $contents : htmlspecialchars($this->label); + $widget = $this->field->widget; + $id = (isset($widget->attrs['id'])) ? $widget->attrs['id'] : $this->autoId(); + $_tmp = array(); + foreach ($attrs as $attr=>$val) { + $_tmp[] = $attr.'="'.$val.'"'; + } + if (count($_tmp)) { + $attrs = ' '.implode(' ', $_tmp); + } else { + $attrs = ''; + } + return new IPF_Template_SafeString(sprintf('', + $widget->idForLabel($id), $attrs, $contents), true); + } + + public function autoId() + { + $id_fields = $this->form->id_fields; + if (false !== strpos($id_fields, '%s')) { + return sprintf($id_fields, $this->html_name); + } elseif ($id_fields) { + return $this->html_name; + } + return ''; + } + + public function fieldErrors() + { + IPF::loadFunction('IPF_Form_renderErrorsAsHTML'); + return new IPF_Template_SafeString(IPF_Form_renderErrorsAsHTML($this->errors), true); + } + + public function __get($prop) + { + if (!in_array($prop, array('labelTag', 'fieldErrors', 'render_w'))) { + return $this->$prop; + } + return $this->$prop(); + } + + public function __toString() + { + return (string)$this->render_w(); + } +} + +if (!function_exists('mb_ucfirst')) { + function mb_ucfirst($str) { + return mb_strtoupper(mb_substr($str, 0, 1)).mb_substr($str, 1); + } +} diff --git a/ipf/form/db.php b/ipf/form/db.php new file mode 100644 index 0000000..5a4fb16 --- /dev/null +++ b/ipf/form/db.php @@ -0,0 +1,46 @@ +value = $value; + $this->column = $column; + $this->extra = array_merge($this->extra, $extra); + } + + function formField($def, $form_field='IPF_Form_Field_Varchar') + { + $defaults = array('required' => !$def['blank'], + 'label' => IPF_Utils::humanTitle($def['verbose']), + 'help_text' => $def['help_text']); + unset($def['blank'], $def['verbose'], $def['help_text']); + if (isset($def['default'])) { + $defaults['initial'] = $def['default']; + unset($def['default']); + } + if (isset($def['choices'])) { + $defaults['widget'] = 'IPF_Form_Widget_SelectInput'; + if (isset($def['widget_attrs'])) { + $def['widget_attrs']['choices'] = $def['choices']; + } else { + $def['widget_attrs'] = array('choices' => $def['choices']); + } + } + foreach (array_keys($def) as $key) { + if (!in_array($key, array('max_length','widget', 'label', 'required', + 'initial', 'choices', 'widget_attrs'))) { + unset($def[$key]); + } + } + $params = array_merge($defaults, $def); + return new $form_field($params); + } +} + diff --git a/ipf/form/db/boolean.php b/ipf/form/db/boolean.php new file mode 100644 index 0000000..94fdea9 --- /dev/null +++ b/ipf/form/db/boolean.php @@ -0,0 +1,8 @@ + 200); + + function formField($def, $form_field='IPF_Form_Field_Email'){ + return parent::formField($def, $form_field); + } +} diff --git a/ipf/form/db/integer.php b/ipf/form/db/integer.php new file mode 100644 index 0000000..0d9d002 --- /dev/null +++ b/ipf/form/db/integer.php @@ -0,0 +1,44 @@ +empty_values)) { + $value = ''; + } + if (is_array($value)) { + reset($value); + while (list($i, $val) = each($value)) { + if (!preg_match('/[0-9]+/', $val)) { + throw new IPF_Exception_Form(__('The value must be an integer.')); + } + $this->checkMinMax($val); + $value[$i] = (int) $val; + } + reset($value); + return $value; + } else { + if (!preg_match('/[0-9]+/', $value)) { + throw new IPF_Exception_Form(__('The value must be an integer.')); + } + $this->checkMinMax($value); + } + return (int) $value; + } + + protected function checkMinMax($value) + { + if ($this->max !== null and $value > $this->max) { + throw new IPF_Exception_Form(sprintf(__('Ensure that this value is not greater than %1$d.'), $this->max)); + } + if ($this->min !== null and $value < $this->min) { + throw new IPF_Exception_Form(sprintf(__('Ensure that this value is not lower than %1$d.'), $this->min)); + } + } +} \ No newline at end of file diff --git a/ipf/form/db/string.php b/ipf/form/db/string.php new file mode 100644 index 0000000..90299e2 --- /dev/null +++ b/ipf/form/db/string.php @@ -0,0 +1,6 @@ +$in) { + if ($key !== 'widget_attrs') + $default[$key] = $this->$key; + } + $m = array_merge($default, $params); + foreach ($params as $key=>$in) { + if ($key !== 'widget_attrs') + $this->$key = $m[$key]; + } + $widget_name = $this->widget; + if (isset($params['widget_attrs'])) { + $attrs = $params['widget_attrs']; + } else { + $attrs = array(); + } + $widget = new $widget_name($attrs); + $attrs = $this->widgetAttrs($widget); + if (count($attrs)) { + $widget->attrs = array_merge($widget->attrs, $attrs); + } + $this->widget = $widget; + } + + function clean($value) + { + if ($this->required and in_array($value, $this->empty_values)) { + throw new IPF_Exception_Form(__('This field is required.')); + } + return $value; + } + + public function widgetAttrs($widget) + { + return array(); + } +} + diff --git a/ipf/form/field/boolean.php b/ipf/form/field/boolean.php new file mode 100644 index 0000000..e9ab890 --- /dev/null +++ b/ipf/form/field/boolean.php @@ -0,0 +1,14 @@ +input_formats as $format) { + if (false !== ($date = strptime($value, $format))) { + $day = str_pad($date['tm_mday'], 2, '0', STR_PAD_LEFT); + $month = str_pad($date['tm_mon']+1, 2, '0', STR_PAD_LEFT); + $year = str_pad($date['tm_year']+1900, 4, '0', STR_PAD_LEFT); + return $year.'-'.$month.'-'.$day; + } + } + throw new IPF_Exception_Form(__('Enter a valid date.')); + } +} diff --git a/ipf/form/field/datetime.php b/ipf/form/field/datetime.php new file mode 100644 index 0000000..1ac8883 --- /dev/null +++ b/ipf/form/field/datetime.php @@ -0,0 +1,42 @@ +input_formats as $format) { + if (false !== ($date = strptime($value, $format))) { + $day = str_pad($date['tm_mday'], 2, '0', STR_PAD_LEFT); + $month = str_pad($date['tm_mon']+1, 2, '0', STR_PAD_LEFT); + $year = str_pad($date['tm_year']+1900, 4, '0', STR_PAD_LEFT); + $h = str_pad($date['tm_hour'], 2, '0', STR_PAD_LEFT); + $m = str_pad($date['tm_min'], 2, '0', STR_PAD_LEFT); + $s = $date['tm_sec']; + if ($s > 59) $s=59; + $s = str_pad($s, 2, '0', STR_PAD_LEFT); + $out = $year.'-'.$month.'-'.$day.' '.$h.':'.$m.':'.$s; + break; + } + } + if ($out !== null) { + // We internally use GMT, so we convert it to a GMT date. + return gmdate('Y-m-d H:i:s', strtotime($out)); + } + throw new IPF_Exception_Form(__('Enter a valid date/time.')); + } +} diff --git a/ipf/form/field/email.php b/ipf/form/field/email.php new file mode 100644 index 0000000..84e19b3 --- /dev/null +++ b/ipf/form/field/email.php @@ -0,0 +1,15 @@ +empty_values)) + return ''; + + if (!IPF_Utils::isEmail($value)) { + throw new IPF_Exception_Form(__('Enter a valid email address.')); + } + return $value; + } +} diff --git a/ipf/form/field/file.php b/ipf/form/field/file.php new file mode 100644 index 0000000..e7d7621 --- /dev/null +++ b/ipf/form/field/file.php @@ -0,0 +1,61 @@ + $this->max_size) { + throw new IPF_Exception_Form(sprintf(__('The uploaded file is to big (%1$s). Reduce the size to less than %2$s and try again.'), + IPF_Utils::prettySize($value['size']), + IPF_Utils::prettySize($this->max_size))); + } + IPF::loadFunction($this->move_function); + return call_user_func($this->move_function, $value, $this->move_function_params); + } +} + +function IPF_Form_Field_moveToUploadFolder($value, $params=array()) +{ + $name = IPF_Utils::cleanFileName($value['name']); + $upload_path = IPF::get('upload_path', '/tmp'); + if (isset($params['upload_path'])) { + $upload_path = $params['upload_path']; + } + $dest = $upload_path.'/'.$name; + if (!@move_uploaded_file($value['tmp_name'], $dest)) { + throw new IPF_Exception_Form(__('An error occured when upload the file. Please try to send the file again.')); + } + @chmod($dest, 0666); + return $name; +} diff --git a/ipf/form/field/float.php b/ipf/form/field/float.php new file mode 100644 index 0000000..ca67fb3 --- /dev/null +++ b/ipf/form/field/float.php @@ -0,0 +1,28 @@ +empty_values)) { + $value = ''; + } + $_value = $value; + $value = (float) $value; + if ((string) $value !== (string) $_value) { + throw new IPF_Exception_Form(__('Enter a number.')); + } + if ($this->max_value !== null and $this->max_value < $value) { + throw new IPF_Exception_Form(sprintf(__('Ensure this value is less than or equal to %s.'), $this->max_value)); + } + if ($this->min_value !== null and $this->min_value > $value) { + throw new IPF_Exception_Form(sprintf(__('Ensure this value is greater than or equal to %s.'), $this->min_value)); + } + return $value; + } +} diff --git a/ipf/form/field/integer.php b/ipf/form/field/integer.php new file mode 100644 index 0000000..e7909dc --- /dev/null +++ b/ipf/form/field/integer.php @@ -0,0 +1,45 @@ +empty_values)) { + $value = ''; + } + if (is_array($value)) { + reset($value); + while (list($i, $val) = each($value)) { + if (!preg_match('/[0-9]+/', $val)) { + throw new IPF_Exception_Form(__('The value must be an integer.')); + } + $this->checkMinMax($val); + $value[$i] = (int) $val; + } + reset($value); + return $value; + } else { + if (!preg_match('/[0-9]+/', $value)) { + throw new IPF_Exception_Form(__('The value must be an integer.')); + } + $this->checkMinMax($value); + } + return (int) $value; + } + + protected function checkMinMax($value) + { + if ($this->max !== null and $value > $this->max) { + throw new IPF_Exception_Form(sprintf(__('Ensure that this value is not greater than %1$d.'), $this->max)); + } + if ($this->min !== null and $value < $this->min) { + throw new IPF_Exception_Form(sprintf(__('Ensure that this value is not lower than %1$d.'), $this->min)); + } + } +} + diff --git a/ipf/form/field/url.php b/ipf/form/field/url.php new file mode 100644 index 0000000..eb5cb65 --- /dev/null +++ b/ipf/form/field/url.php @@ -0,0 +1,18 @@ +empty_values)) { + return ''; + } + if (!IPF_Utils::isValidUrl($value)) { + throw new IPF_Exception_Form(__('Enter a valid address.')); + } + return $value; + } +} diff --git a/ipf/form/field/varchar.php b/ipf/form/field/varchar.php new file mode 100644 index 0000000..723b446 --- /dev/null +++ b/ipf/form/field/varchar.php @@ -0,0 +1,30 @@ +empty_values)) { + $value = ''; + } + $value_length = mb_strlen($value); + if ($this->max_length !== null and $value_length > $this->max_length) { + throw new IPF_Exception_Form(sprintf(__('Ensure this value has at most %1$d characters (it has %2$d).'), $this->max_length, $value_length)); + } + if ($this->min_length !== null and $value_length < $this->min_length) { + throw new IPF_Exception_Form(sprintf(__('Ensure this value has at least %1$d characters (it has %2$d).'), $this->min_length, $value_length)); + } + return $value; + } + + public function widgetAttrs($widget){ + if ($this->max_length !== null and in_array(get_class($widget), array('IPF_Form_Widget_TextInput', 'IPF_Form_Widget_PasswordInput'))) { + return array('maxlength'=>$this->max_length); + } + return array(); + } +} + diff --git a/ipf/form/fieldproxy.php b/ipf/form/fieldproxy.php new file mode 100644 index 0000000..2a23028 --- /dev/null +++ b/ipf/form/fieldproxy.php @@ -0,0 +1,16 @@ +form = $form; + } + + public function __get($field) + { + return new IPF_Form_BoundField($this->form, $this->form->fields[$field], $field); + } +} diff --git a/ipf/form/model.php b/ipf/form/model.php new file mode 100644 index 0000000..1c5d2a2 --- /dev/null +++ b/ipf/form/model.php @@ -0,0 +1,80 @@ +model = $extra['model']; + if (isset($extra['user_fields'])) + $this->user_fields = $extra['user_fields']; + + + $user_fields = $this->fields(); + $db_columns = $this->model->getTable()->getColumns(); + + + if ($user_fields===null){ + foreach($db_columns as $name=>$col){ + $this->addDBField($name,$col); + } + } + else{ + foreach($user_fields as $uname){ + if (array_key_exists($uname,$db_columns)) + $this->addDBField($uname,$db_columns[$uname]); + else{ + $add_method = 'add__'.$uname.'__field'; + $this->$add_method(); + } + } + } + } + + function addDBField($name,$col){ + if ($name==$this->model->getTable()->getIdentifier()) + return; + + $defaults = array('blank' => true, 'verbose' => $name, 'help_text' => '', 'editable' => true); + $type = $col['type']; + if (isset($col['notblank'])) + $defaults['blank'] = false; + if (isset($col['length'])) + $defaults['max_length'] = (int)($col['length']); + if (isset($col['email'])) + $type = 'email'; + $cn = 'IPF_Form_DB_'.$type; + $db_field = new $cn('', $name); + + //echo $name; + //print_r($defaults); + + if (null !== ($form_field=$db_field->formField($defaults))) { + $this->fields[$name] = $form_field; + } + } + + function fields(){ return $this->user_fields; } + + function save($commit=true) + { + if ($this->isValid()) { + $this->model->SetFromFormData($this->cleaned_data); + + //print_r($this->model->data); + + /* + if ($commit && $this->model->id) { + $this->model->update(); + } elseif ($commit) { + $this->model->create(); + } + */ + print_r($this->model->save()); + return $this->model; + } + throw new Exception(__('Cannot save the model from an invalid form.')); + } +} diff --git a/ipf/form/widget.php b/ipf/form/widget.php new file mode 100644 index 0000000..551fd41 --- /dev/null +++ b/ipf/form/widget.php @@ -0,0 +1,46 @@ +attrs = $attrs; + } + + public function render($name, $value, $extra_attrs=array()) + { + throw new IPF_Exception('Not Implemented.'); + } + + protected function buildAttrs($attrs, $extra_attrs=array()) + { + return array_merge($this->attrs, $attrs, $extra_attrs); + } + + public function valueFromFormData($name, $data) + { + if (isset($data[$name])) { + return $data[$name]; + } + return null; + } + + public function idForLabel($id) + { + return $id; + } +} + +function IPF_Form_Widget_Attrs($attrs) +{ + $_tmp = array(); + foreach ($attrs as $attr=>$val) { + $_tmp[] = $attr.'="'.$val.'"'; + } + return ' '.implode(' ', $_tmp); +} \ No newline at end of file diff --git a/ipf/form/widget/checkboxinput.php b/ipf/form/widget/checkboxinput.php new file mode 100644 index 0000000..d365982 --- /dev/null +++ b/ipf/form/widget/checkboxinput.php @@ -0,0 +1,23 @@ + 0) { + $value = date($this->format, strtotime($value.' GMT')); + } + return parent::render($name, $value, $extra_attrs); + } +} \ No newline at end of file diff --git a/ipf/form/widget/fileinput.php b/ipf/form/widget/fileinput.php new file mode 100644 index 0000000..ae03788 --- /dev/null +++ b/ipf/form/widget/fileinput.php @@ -0,0 +1,14 @@ + '70', + 'rows' => '20'); + $config = array('tinymce_url', 'mode', 'theme', 'include_tinymce'); + foreach ($config as $cfg) { + if (isset($attrs[$cfg])) { + $this->$cfg = $attrs[$cfg]; + unset($attrs[$cfg]); + } + } + $this->attrs = array_merge($defaults, $attrs); + } + + public function render($name, $value, $extra_attrs=array()) + { + if ($value === null) $value = ''; + $extra_config = ''; + if (isset($this->attrs['editor_config'])) { + $_ec = $this->attrs['editor_config']; + unset($this->attrs['editor_config']); + $_st = array(); + foreach ($_ec as $key=>$val) { + if (is_bool($val)) { + if ($val) { + $_st[] = $key.' : true'; + } else { + $_st[] = $key.' : false'; + } + } else { + $_st[] = $key.' : "'.$val.'"'; + } + } + if ($_st) { + $extra_config = ",\n".implode(",\n", $_st); + } + } + $final_attrs = $this->buildAttrs(array('name' => $name), + $extra_attrs); + // The special include for tinyMCE + $out = ''; + if ($this->include_tinymce) { + $out .= ''."\n"; + } + $out .=''; + return new IPF_Template_SafeString( + $out.sprintf('%s', + IPF_Form_Widget_Attrs($final_attrs), + htmlspecialchars($value, ENT_COMPAT, 'UTF-8')), + true); + } +} \ No newline at end of file diff --git a/ipf/form/widget/input.php b/ipf/form/widget/input.php new file mode 100644 index 0000000..81be6ba --- /dev/null +++ b/ipf/form/widget/input.php @@ -0,0 +1,17 @@ +buildAttrs(array('name' => $name, + 'type' => $this->input_type), + $extra_attrs); + if ($value !== '') { + $value = htmlspecialchars($value, ENT_COMPAT, 'UTF-8'); + $final_attrs['value'] = $value; + } + return new IPF_Template_SafeString('', true); + } +} \ No newline at end of file diff --git a/ipf/form/widget/passwordinput.php b/ipf/form/widget/passwordinput.php new file mode 100644 index 0000000..b0823ac --- /dev/null +++ b/ipf/form/widget/passwordinput.php @@ -0,0 +1,22 @@ +render_value = (isset($attrs['render_value'])) ? $attrs['render_value'] : $this->render_value; + unset($attrs['render_value']); + parent::__construct($attrs); + } + + public function render($name, $value, $extra_attrs=array()) + { + if ($this->render_value === false) { + $value = ''; + } + return parent::render($name, $value, $extra_attrs); + } +} \ No newline at end of file diff --git a/ipf/form/widget/selectinput.php b/ipf/form/widget/selectinput.php new file mode 100644 index 0000000..12b461e --- /dev/null +++ b/ipf/form/widget/selectinput.php @@ -0,0 +1,34 @@ +choices = $attrs['choices']; + unset($attrs['choices']); + parent::__construct($attrs); + } + + public function render($name, $value, $extra_attrs=array(), + $choices=array()) + { + $output = array(); + if ($value === null) { + $value = ''; + } + $final_attrs = $this->buildAttrs(array('name' => $name), $extra_attrs); + $output[] = ''; + $choices = $this->choices + $choices; + foreach ($choices as $option_label=>$option_value) { + $selected = ($option_value == $value) ? ' selected="selected"':''; + $output[] = sprintf('', + htmlspecialchars($option_value, ENT_COMPAT, 'UTF-8'), + $selected, + htmlspecialchars($option_label, ENT_COMPAT, 'UTF-8')); + } + $output[] = ''; + return new IPF_Template_SafeString(implode("\n", $output), true); + } +} \ No newline at end of file diff --git a/ipf/form/widget/selectmultipleinput.php b/ipf/form/widget/selectmultipleinput.php new file mode 100644 index 0000000..e3526e5 --- /dev/null +++ b/ipf/form/widget/selectmultipleinput.php @@ -0,0 +1,46 @@ +choices = $attrs['choices']; + unset($attrs['choices']); + parent::__construct($attrs); + } + + public function render($name, $value, $extra_attrs=array(), + $choices=array()) + { + $output = array(); + if ($value === null) { + $value = array(); + } + $final_attrs = $this->buildAttrs(array('name' => $name.'[]'), + $extra_attrs); + $output[] = ''; + return new IPF_Template_SafeString(implode("\n", $output), true); + } + + public function valueFromFormData($name, $data) + { + if (isset($data[$name]) and is_array($data[$name])) { + return $data[$name]; + } + return null; + } + +} \ No newline at end of file diff --git a/ipf/form/widget/selectmultipleinputcheckbox.php b/ipf/form/widget/selectmultipleinputcheckbox.php new file mode 100644 index 0000000..d2b18f6 --- /dev/null +++ b/ipf/form/widget/selectmultipleinputcheckbox.php @@ -0,0 +1,41 @@ +buildAttrs($extra_attrs); + $output[] = '
      '; + $choices = array_merge($this->choices, $choices); + $i=0; + $base_id = $final_attrs['id']; + foreach ($choices as $option_label=>$option_value) { + + $final_attrs['id'] = $base_id.'_'.$i; + $final_attrs['value'] = htmlspecialchars($option_value, ENT_COMPAT, 'UTF-8'); + $checkbox = new IPF_Form_Widget_CheckboxInput($final_attrs); + $rendered = $checkbox->render($name.'[]', in_array($option_value, $value)); + + $output[] = sprintf('
    • ', $rendered, + htmlspecialchars($option_label, ENT_COMPAT, 'UTF-8')); + $i++; + } + $output[] = '
    '; + return new IPF_Template_SafeString(implode("\n", $output), true); + } + + public function idForLabel($id) + { + if ($id) { + $id += '_0'; + } + return $id; + } + + +} \ No newline at end of file diff --git a/ipf/form/widget/textareainput.php b/ipf/form/widget/textareainput.php new file mode 100644 index 0000000..bab0cf8 --- /dev/null +++ b/ipf/form/widget/textareainput.php @@ -0,0 +1,23 @@ +attrs = array_merge(array('cols' => '40', 'rows' => '10'), + $attrs); + } + + public function render($name, $value, $extra_attrs=array()) + { + if ($value === null) $value = ''; + $final_attrs = $this->buildAttrs(array('name' => $name), + $extra_attrs); + return new IPF_Template_SafeString( + sprintf('%s', + IPF_Form_Widget_Attrs($final_attrs), + htmlspecialchars($value, ENT_COMPAT, 'UTF-8')), + true); + } +} diff --git a/ipf/form/widget/textinput.php b/ipf/form/widget/textinput.php new file mode 100644 index 0000000..c8875c9 --- /dev/null +++ b/ipf/form/widget/textinput.php @@ -0,0 +1,6 @@ + 1 && $arg{1} == '-' && !$long_options)) { + $non_opts = array_merge($non_opts, array_slice($args, $i)); + break; + } elseif (strlen($arg) > 1 && $arg{1} == '-') { + $error = IPF_Getopt::_parseLongOption(substr($arg, 2), $long_options, $opts, $args); + } elseif ($arg == '-') { + // - is stdin + $non_opts = array_merge($non_opts, array_slice($args, $i)); + break; + } else { + $error = IPF_Getopt::_parseShortOption(substr($arg, 1), $short_options, $opts, $args); + } + } + + return array($opts, $non_opts); + } + + /** + * @access private + * + */ + function _parseShortOption($arg, $short_options, &$opts, &$args) + { + for ($i = 0; $i < strlen($arg); $i++) { + $opt = $arg{$i}; + $opt_arg = null; + + /* Try to find the short option in the specifier string. */ + if (($spec = strstr($short_options, $opt)) === false || $arg{$i} == ':') + { + throw new IPF_Exception("IPF_Getopt: unrecognized option -- $opt"); + } + + if (strlen($spec) > 1 && $spec{1} == ':') { + if (strlen($spec) > 2 && $spec{2} == ':') { + if ($i + 1 < strlen($arg)) { + /* Option takes an optional argument. Use the remainder of + the arg string if there is anything left. */ + $opts[] = array($opt, substr($arg, $i + 1)); + break; + } + } else { + /* Option requires an argument. Use the remainder of the arg + string if there is anything left. */ + if ($i + 1 < strlen($arg)) { + $opts[] = array($opt, substr($arg, $i + 1)); + break; + } else if (list(, $opt_arg) = each($args)) { + /* Else use the next argument. */; + if (IPF_Getopt::_isShortOpt($opt_arg) || IPF_Getopt::_isLongOpt($opt_arg)) { + throw new IPF_Exception("IPF_Getopt: option requires an argument -- $opt"); + } + } else { + throw new IPF_Exception("IPF_Getopt: option requires an argument -- $opt"); + } + } + } + + $opts[] = array($opt, $opt_arg); + } + } + + /** + * @access private + * + */ + function _isShortOpt($arg) + { + return strlen($arg) == 2 && $arg[0] == '-' && preg_match('/[a-zA-Z]/', $arg[1]); + } + + /** + * @access private + * + */ + function _isLongOpt($arg) + { + return strlen($arg) > 2 && $arg[0] == '-' && $arg[1] == '-' && + preg_match('/[a-zA-Z]+$/', substr($arg, 2)); + } + + /** + * @access private + * + */ + function _parseLongOption($arg, $long_options, &$opts, &$args) + { + @list($opt, $opt_arg) = explode('=', $arg, 2); + $opt_len = strlen($opt); + + for ($i = 0; $i < count($long_options); $i++) { + $long_opt = $long_options[$i]; + $opt_start = substr($long_opt, 0, $opt_len); + $long_opt_name = str_replace('=', '', $long_opt); + + /* Option doesn't match. Go on to the next one. */ + if ($long_opt_name != $opt) { + continue; + } + + $opt_rest = substr($long_opt, $opt_len); + + /* Check that the options uniquely matches one of the allowed + options. */ + if ($i + 1 < count($long_options)) { + $next_option_rest = substr($long_options[$i + 1], $opt_len); + } else { + $next_option_rest = ''; + } + if ($opt_rest != '' && $opt{0} != '=' && + $i + 1 < count($long_options) && + $opt == substr($long_options[$i+1], 0, $opt_len) && + $next_option_rest != '' && + $next_option_rest{0} != '=') { + throw new IPF_Exception("IPF_Getopt: option --$opt is ambiguous"); + } + + if (substr($long_opt, -1) == '=') { + if (substr($long_opt, -2) != '==') { + /* Long option requires an argument. + Take the next argument if one wasn't specified. */; + if (!strlen($opt_arg) && !(list(, $opt_arg) = each($args))) { + throw new IPF_Exception("IPF_Getopt: option --$opt requires an argument"); + } + if (IPF_Getopt::_isShortOpt($opt_arg) || IPF_Getopt::_isLongOpt($opt_arg)) { + throw new IPF_Exception("IPF_Getopt: option requires an argument --$opt"); + } + } + } else if ($opt_arg) { + throw new IPF_Exception("IPF_Getopt: option --$opt doesn't allow an argument"); + } + + $opts[] = array('--' . $opt, $opt_arg); + return; + } + throw new IPF_Exception("IPF_Getopt: unrecognized option --$opt"); + } + + function readPHPArgv() + { + global $argv; + if (!is_array($argv)) { + if (!@is_array($_SERVER['argv'])) { + if (!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) { + throw new IPF_Exception("IPF_Getopt: Could not read cmd args (register_argc_argv=Off?)"); + } + return $GLOBALS['HTTP_SERVER_VARS']['argv']; + } + return $_SERVER['argv']; + } + return $argv; + } +} + +?> diff --git a/ipf/http.php b/ipf/http.php new file mode 100644 index 0000000..bd4f02a --- /dev/null +++ b/ipf/http.php @@ -0,0 +1,50 @@ + $v) { + if (is_array($v)) { + $result[$k] = IPF_HTTP_handleMagicQuotes($v); + } else { + $result[$k] = stripslashes($v); + } + } + return $result; + } else { + return stripslashes($value); + } +} diff --git a/ipf/http/request.php b/ipf/http/request.php new file mode 100644 index 0000000..22a9a83 --- /dev/null +++ b/ipf/http/request.php @@ -0,0 +1,34 @@ +removeTheMagic(); + $this->POST =& $_POST; + $this->GET =& $_GET; + $this->REQUEST =& $_REQUEST; + $this->COOKIE =& $_COOKIE; + $this->FILES =& $_FILES; + $this->query = $query; + $this->method = $_SERVER['REQUEST_METHOD']; + $this->uri = $_SERVER['REQUEST_URI']; + $this->remote_addr = $_SERVER['REMOTE_ADDR']; + $this->http_host = $_SERVER['HTTP_HOST']; + $this->SERVER =& $_SERVER; + } +} diff --git a/ipf/http/response.php b/ipf/http/response.php new file mode 100644 index 0000000..1f279f7 --- /dev/null +++ b/ipf/http/response.php @@ -0,0 +1,103 @@ + 'CONTINUE', + '101' => 'SWITCHING PROTOCOLS', + '200' => 'OK', + '201' => 'CREATED', + '202' => 'ACCEPTED', + '203' => 'NON-AUTHORITATIVE INFORMATION', + '204' => 'NO CONTENT', + '205' => 'RESET CONTENT', + '206' => 'PARTIAL CONTENT', + '300' => 'MULTIPLE CHOICES', + '301' => 'MOVED PERMANENTLY', + '302' => 'FOUND', + '303' => 'SEE OTHER', + '304' => 'NOT MODIFIED', + '305' => 'USE PROXY', + '306' => 'RESERVED', + '307' => 'TEMPORARY REDIRECT', + '400' => 'BAD REQUEST', + '401' => 'UNAUTHORIZED', + '402' => 'PAYMENT REQUIRED', + '403' => 'FORBIDDEN', + '404' => 'NOT FOUND', + '405' => 'METHOD NOT ALLOWED', + '406' => 'NOT ACCEPTABLE', + '407' => 'PROXY AUTHENTICATION REQUIRED', + '408' => 'REQUEST TIMEOUT', + '409' => 'CONFLICT', + '410' => 'GONE', + '411' => 'LENGTH REQUIRED', + '412' => 'PRECONDITION FAILED', + '413' => 'REQUEST ENTITY TOO LARGE', + '414' => 'REQUEST-URI TOO LONG', + '415' => 'UNSUPPORTED MEDIA TYPE', + '416' => 'REQUESTED RANGE NOT SATISFIABLE', + '417' => 'EXPECTATION FAILED', + '500' => 'INTERNAL SERVER ERROR', + '501' => 'NOT IMPLEMENTED', + '502' => 'BAD GATEWAY', + '503' => 'SERVICE UNAVAILABLE', + '504' => 'GATEWAY TIMEOUT', + '505' => 'HTTP VERSION NOT SUPPORTED' + ); + + function __construct($content='', $mimetype=null) + { + if (is_null($mimetype)) { + $mimetype = IPF::get('mimetype', 'text/html').'; charset=utf-8'; + } + $this->content = $content; + $this->headers['Content-Type'] = $mimetype; + $this->headers['X-Powered-By'] = 'IPF - http://ipf.icmconsulting.com/'; + $this->status_code = 200; + $this->cookies = array(); + } + + function render($output_body=true) + { + if ($this->status_code >= 200 + && $this->status_code != 204 + && $this->status_code != 304) { + $this->headers['Content-Length'] = strlen($this->content); + } + $this->outputHeaders(); + if ($output_body) { + echo($this->content); + } + } + + function outputHeaders() + { + if (!defined('IN_UNIT_TESTS')) { + header('HTTP/1.1 '.$this->status_code.' ' + .$this->status_code_list[$this->status_code], + true, $this->status_code); + foreach ($this->headers as $header => $ch) { + header($header.': '.$ch); + } + foreach ($this->cookies as $cookie => $data) { + // name, data, expiration, path, domain, secure, http only + setcookie($cookie, $data, + time()+31536000, + IPF::get('cookie_path', '/'), + IPF::get('cookie_domain', null), + IPF::get('cookie_secure', false), + IPF::get('cookie_httponly', true)); + } + } else { + $_COOKIE = array(); + foreach ($this->cookies as $cookie => $data) { + $_COOKIE[$cookie] = $data; + } + } + } +} diff --git a/ipf/http/response/notfound.php b/ipf/http/response/notfound.php new file mode 100644 index 0000000..e23a826 --- /dev/null +++ b/ipf/http/response/notfound.php @@ -0,0 +1,10 @@ +status_code = 404; + } +} diff --git a/ipf/http/response/redirect.php b/ipf/http/response/redirect.php new file mode 100644 index 0000000..051a2b5 --- /dev/null +++ b/ipf/http/response/redirect.php @@ -0,0 +1,12 @@ +Please, click here to be redirected.'), $url); + parent::__construct($content); + $this->headers['Location'] = $url; + $this->status_code = 302; + } +} diff --git a/ipf/http/response/servererror.php b/ipf/http/response/servererror.php new file mode 100644 index 0000000..7eeea86 --- /dev/null +++ b/ipf/http/response/servererror.php @@ -0,0 +1,10 @@ +status_code = 500; + } +} diff --git a/ipf/http/response/servererrordebug.php b/ipf/http/response/servererrordebug.php new file mode 100644 index 0000000..d4374cb --- /dev/null +++ b/ipf/http/response/servererrordebug.php @@ -0,0 +1,346 @@ +status_code = 500; + $this->content = IPF_HTTP_Response_ServerErrorDebug_Pretty($e); + } +} + +function IPF_HTTP_Response_ServerErrorDebug_Pretty($e) +{ + $o = create_function('$in','return htmlspecialchars($in);'); + $sub = create_function('$f','$loc="";if(isset($f["class"])){ + $loc.=$f["class"].$f["type"];} + if(isset($f["function"])){$loc.=$f["function"];} + if(!empty($loc)){$loc=htmlspecialchars($loc); + $loc="$loc";}return $loc;'); + $parms = create_function('$f','$params=array();if(isset($f["function"])){ + try{if(isset($f["class"])){ + $r=new ReflectionMethod($f["class"]."::".$f["function"]);} + else{$r=new ReflectionFunction($f["function"]);} + return $r->getParameters();}catch(Exception $e){}} + return $params;'); + $src2lines = create_function('$file','$src=nl2br(highlight_file($file,TRUE)); + return explode("
    ",$src);'); + $clean = create_function('$line','return trim(strip_tags($line));'); + $desc = get_class($e)." making ".$_SERVER['REQUEST_METHOD']." request to ". + $_SERVER['REQUEST_URI']; + $out = ' + + + + + + '.$o($desc).' + + + + + +
    +

    '.$o($desc).'

    +

    '; + if ($e->getCode()) { + $out .= $o($e->getCode()). ' : '; + } + $out .= ' '.$o($e->getMessage()).'

    + + + + + + + + + +
    PHP'.$o($e->getFile()).', line '.$o($e->getLine()).'
    URI'.$o($_SERVER['REQUEST_METHOD'].' '. + $_SERVER['REQUEST_URI']).'
    +
    + +
    +

    Stacktrace + + ▶

    +
      '; + $frames = $e->getTrace(); + foreach ($frames as $frame_id=>$frame) { + if (!isset($frame['file'])) { + $frame['file'] = 'No File'; + $frame['line'] = '0'; + } + $out .= '
    • '.$sub($frame).' + ['.$o($frame['file']).', line '.$o($frame['line']).']'; + if (isset($frame['args']) && count($frame['args']) > 0) { + $params = $parms($frame); + $out .= ' + + + + + + + + + + '; + foreach ($frame['args'] as $k => $v) { + $name = isset($params[$k]) ? '$'.$params[$k]->name : '?'; + $out .= ' + + + + + '; + } + $out .= '
      ArgNameValue
      '.$o($k).''.$o($name).' +
      '.highlight_string(print_r($v,true), true).'
      +
      '; + } + if (is_readable($frame['file']) ) { + $out .= ' + +
      '; + $lines = $src2lines($frame['file']); + $start = $frame['line'] < 5 ? + 0 : $frame['line'] -5; $end = $start + 10; + $out2 = ''; + foreach ( $lines as $k => $line ) { + if ( $k > $end ) { break; } + $line = trim(strip_tags($line)); + if ( $k < $start && isset($frames[$frame_id+1]["function"]) + && preg_match('/function( )*'.preg_quote($frames[$frame_id+1]["function"]).'/', + $line) ) { + $start = $k; + } + if ( $k >= $start ) { + if ( $k != $frame['line'] ) { + $out2 .= '
    • '.$clean($line).'
    • '."\n"; } + else { + $out2 .= '
    • '. + $clean($line).'
    • '."\n"; } + } + } + $out .= "
        \n".$out2. "
      \n"; + $out .= '
    '; + } else { + $out .= '
    No src available
    '; + } + $out .= ''; + } + $out .= ' + + + + +
    +

    Request + + ▶

    +
    '; + if ( function_exists('apache_request_headers') ) { + $out .= '

    Request (raw)

    '; + $req_headers = apache_request_headers(); + $out .= '

    HEADERS

    '; + if ( count($req_headers) > 0 ) { + $out .= '

    '; + foreach ($req_headers as $req_h_name => $req_h_val) { + $out .= $o($req_h_name.': '.$req_h_val); + $out .= '
    '; + } + $out .= '

    '; + } else { + $out .= '

    No headers.

    '; + } + $req_body = file_get_contents('php://input'); + if ( strlen( $req_body ) > 0 ) { + $out .=' +

    Body

    +

    + '.$o($req_body).' +

    '; + } + } + $out .= ' +

    Request (parsed)

    '; + $superglobals = array('$_GET','$_POST','$_COOKIE','$_SERVER','$_ENV'); + foreach ( $superglobals as $sglobal ) { + $sfn = create_function('','return '.$sglobal.';'); + $out .= '

    '.$sglobal.'

    '; + if ( count($sfn()) > 0 ) { + $out .= ' + + + + + + + + '; + foreach ( $sfn() as $k => $v ) { + $out .= ' + + + '; + } + $out .= ' + +
    VariableValue
    '.$o($k).' +
    '.$o(print_r($v,TRUE)).'
    +
    '; + } else { + $out .= ' +

    No data

    '; + } + } + $out .= ' + +
    +
    '; + if ( function_exists('headers_list') ) { + $out .= ' +
    + +

    Response + + ▶

    + +
    + +

    Headers

    '; + $resp_headers = headers_list(); + if (count($resp_headers) > 0) { + $out .= ' +

    '; + foreach ( $resp_headers as $resp_h ) { + $out .= $o($resp_h); + $out .= '
    '; + } + $out .= '

    '; + } else { + $out .= ' +

    No headers.

    '; + } + $out .= ' +
    '; + } + $out .= ' + + +'; + return $out; +} + diff --git a/ipf/http/url.php b/ipf/http/url.php new file mode 100644 index 0000000..26d5e8d --- /dev/null +++ b/ipf/http/url.php @@ -0,0 +1,95 @@ + 0) { + $url .= '?'; + $params_list = array(); + foreach ($params as $key=>$value) { + $params_list[] = urlencode($key).'='.urlencode($value); + } + $url .= implode($amp, $params_list); + } + return $url; + } + + public static function getAction() + { + if (isset($_SERVER['ORIG_PATH_INFO'])) { + return $_SERVER['ORIG_PATH_INFO']; + } + if (isset($_SERVER['PATH_INFO'])) { + return $_SERVER['PATH_INFO']; + } + return '/'; + } +} + +function IPF_HTTP_URL_urlForView($view, $params=array(), $by_name=false, + $get_params=array(), $encoded=true) +{ + $action = IPF_HTTP_URL_reverse($view, $params, $by_name); + return IPF_HTTP_URL::generate($action, $get_params, $encoded); +} + +function IPF_HTTP_URL_reverse($view, $params=array(), $by_name=false) +{ + $regex = null; + + foreach (IPF::get('urls') as $url) { + $prefix = $url['prefix']; + foreach ($url['urls'] as $suburl){ + if ($suburl['func']==$view){ + $regex = $prefix.$suburl['regex']; + break; + } + } + if ($regex!==null) + break; + } + if ($regex === null) { + throw new IPF_Exception('Error, the view: '.$view.' has not been found.'); + } + $url = IPF_HTTP_URL_buildReverseUrl($regex, $params); + return IPF::get('app_base').$url; +} + +function IPF_HTTP_URL_buildReverseUrl($url_regex, $params=array()) +{ + $url_regex = str_replace('\\.', '.', $url_regex); + $url_regex = str_replace('\\-', '-', $url_regex); + $url = $url_regex; + $groups = '#\(([^)]+)\)#'; + $matches = array(); + preg_match_all($groups, $url_regex, $matches); + reset($params); + if (count($matches[0]) && count($matches[0]) == count($params)) { + // Test the params against the pattern + foreach ($matches[0] as $pattern) { + $in = current($params); + if (0 === preg_match('#'.$pattern.'#', $in)) { + throw new IPF_Exception('Error, param: '.$in.' is not matching the pattern: '.$pattern); + } + next($params); + } + $func = create_function('$matches', + 'static $p = '.var_export($params, true).'; '. + '$a = current($p); '. + 'next($p); '. + 'return $a;'); + $url = preg_replace_callback($groups, $func, $url_regex); + } + $url = substr(substr($url, 2), 0, -2); + if (substr($url, -1) !== '$') { + return $url; + } + return substr($url, 0, -1); +} diff --git a/ipf/orm.php b/ipf/orm.php new file mode 100644 index 0000000..ecb2556 --- /dev/null +++ b/ipf/orm.php @@ -0,0 +1,333 @@ +getConnectionForComponent($componentName)->getTable($componentName); + } + + public static function generateModelsFromYaml($yamlPath, $directory, $options = array()) + { + $import = new IPF_ORM_Import_Schema(); + $import->setOptions($options); + $import->importSchema($yamlPath, 'yml', $directory); + } + + public static function createTablesFromModels($directory) + { + return IPF_ORM_Manager::connection()->export->exportSchema($directory); + } + + public static function generateSqlFromModels($directory = null) + { + $sql = IPF_ORM_Manager::connection()->export->exportSql($directory); + $build = ''; + foreach ($sql as $query) { + $build .= $query.";\n"; + } + return $build; + } + + public static function loadModel($className, $path = null) + { + self::$_loadedModelFiles[$className] = $path; + } + + public static function filterInvalidModels($classes) + { + $validModels = array(); + foreach ((array) $classes as $name) { + if (self::isValidModelClass($name) && ! in_array($name, $validModels)) { + $validModels[] = $name; + } + } + + return $validModels; + } + + public static function loadModels($directory, $modelLoading = null) + { + $manager = IPF_ORM_Manager::getInstance(); + + $modelLoading = $modelLoading === null ? $manager->getAttribute(IPF_ORM::ATTR_MODEL_LOADING):$modelLoading; + $loadedModels = array(); + if ($directory !== null) { + foreach ((array) $directory as $dir) { + if ( ! is_dir($dir)) { + throw new IPF_ORM_Exception('You must pass a valid path to a directory containing IPF_ORM models'); + } + $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir), RecursiveIteratorIterator::LEAVES_ONLY); + foreach ($it as $file) { + $e = explode('.', $file->getFileName()); + if (end($e) === 'php' && strpos($file->getFileName(), '.inc') === false) { + $className = $e[0]; + if ($modelLoading == IPF_ORM::MODEL_LOADING_CONSERVATIVE) { + self::loadModel($className, $file->getPathName()); + + $loadedModels[$className] = $className; + } else { + //$declaredBefore = get_declared_classes(); + require_once($file->getPathName()); + $loadedModels[$className] = $className; // !!! + + /* + //$declaredAfter = get_declared_classes(); + //$foundClasses = array_slice($declaredAfter, count($declaredBefore) - 1); + if ($foundClasses) { + foreach ($foundClasses as $className) { + if (self::isValidModelClass($className)) { + $loadedModels[$className] = $className; + + self::loadModel($className, $file->getPathName()); + } + } + } + */ + } + } + } + } + } + return $loadedModels; + } + + + public static function isValidModelClass($class) + { + if ($class instanceof IPF_ORM_Record) { + $class = get_class($class); + } + if (is_string($class) && class_exists($class)) { + $class = new ReflectionClass($class); + } + if ($class instanceof ReflectionClass) { + if ( ! $class->isAbstract() && $class->isSubClassOf('IPF_ORM_Record')) { + return true; + } + } + return false; + } + + public static function dump($var, $output = true, $indent = "") + { + $ret = array(); + switch (gettype($var)) { + case 'array': + $ret[] = 'Array('; + $indent .= " "; + foreach ($var as $k => $v) { + + $ret[] = $indent . $k . ' : ' . self::dump($v, false, $indent); + } + $indent = substr($indent,0, -4); + $ret[] = $indent . ")"; + break; + case 'object': + $ret[] = 'Object(' . get_class($var) . ')'; + break; + default: + $ret[] = var_export($var, true); + } + + if ($output) { + print implode("\n", $ret); + } + + return implode("\n", $ret); + } +} diff --git a/ipf/orm/access.php b/ipf/orm/access.php new file mode 100644 index 0000000..39ae574 --- /dev/null +++ b/ipf/orm/access.php @@ -0,0 +1,82 @@ + $v) { + $this->set($k, $v); + } + + return $this; + } + + public function __set($name, $value) + { + $this->set($name, $value); + } + + public function __get($name) + { + return $this->get($name); + } + + public function __isset($name) + { + return $this->contains($name); + } + + public function __unset($name) + { + return $this->remove($name); + } + + public function offsetExists($offset) + { + return $this->contains($offset); + } + + public function offsetGet($offset) + { + return $this->get($offset); + } + + public function offsetSet($offset, $value) + { + if ( ! isset($offset)) { + $this->add($value); + } else { + $this->set($offset, $value); + } + } + + public function offsetUnset($offset) + { + return $this->remove($offset); + } + + public function remove($offset) + { + throw new IPF_ORM_Exception('Remove is not supported for ' . get_class($this)); + } + + public function get($offset) + { + throw new IPF_ORM_Exception('Get is not supported for ' . get_class($this)); + } + + public function set($offset, $value) + { + throw new IPF_ORM_Exception('Set is not supported for ' . get_class($this)); + } + + public function contains($offset) + { + throw new IPF_ORM_Exception('Contains is not supported for ' . get_class($this)); + } + + public function add($value) + { + throw new IPF_ORM_Exception('Add is not supported for ' . get_class($this)); + } +} \ No newline at end of file diff --git a/ipf/orm/adapter.php b/ipf/orm/adapter.php new file mode 100644 index 0000000..06b9b51 --- /dev/null +++ b/ipf/orm/adapter.php @@ -0,0 +1,74 @@ +_table = $table; + + if ($keyColumn === null) { + $keyColumn = $table->getBoundQueryPart('indexBy'); + } + + if ($keyColumn === null) { + $keyColumn = $table->getAttribute(IPF_ORM::ATTR_COLL_KEY); + } + + if ($keyColumn !== null) { + $this->keyColumn = $keyColumn; + } + } + + public static function initNullObject(IPF_ORM_Null $null) + { + self::$null = $null; + } + + public function getTable() + { + return $this->_table; + } + + public function setData(array $data) + { + $this->data = $data; + } + + public function serialize() + { + $vars = get_object_vars($this); + + unset($vars['reference']); + unset($vars['reference_field']); + unset($vars['relation']); + unset($vars['expandable']); + unset($vars['expanded']); + unset($vars['generator']); + + $vars['_table'] = $vars['_table']->getComponentName(); + + return serialize($vars); + } + + public function unserialize($serialized) + { + $manager = IPF_ORM_Manager::getInstance(); + $connection = $manager->getCurrentConnection(); + + $array = unserialize($serialized); + + foreach ($array as $name => $values) { + $this->$name = $values; + } + + $this->_table = $connection->getTable($this->_table); + + $keyColumn = isset($array['keyColumn']) ? $array['keyColumn'] : null; + if ($keyColumn === null) { + $keyColumn = $this->_table->getBoundQueryPart('indexBy'); + } + + if ($keyColumn !== null) { + $this->keyColumn = $keyColumn; + } + } + + public function setKeyColumn($column) + { + $this->keyColumn = $column; + + return $this; + } + + public function getKeyColumn() + { + return $this->keyColumn; + } + + public function getData() + { + return $this->data; + } + + public function getFirst() + { + return reset($this->data); + } + + public function getLast() + { + return end($this->data); + } + + public function end() + { + return end($this->data); + } + + public function key() + { + return key($this->data); + } + + public function setReference(IPF_ORM_Record $record, IPF_ORM_Relation $relation) + { + $this->reference = $record; + $this->relation = $relation; + + if ($relation instanceof IPF_ORM_Relation_ForeignKey || + $relation instanceof IPF_ORM_Relation_LocalKey) { + + $this->referenceField = $relation->getForeignFieldName(); + + $value = $record->get($relation->getLocalFieldName()); + + foreach ($this->data as $record) { + if ($value !== null) { + $record->set($this->referenceField, $value, false); + } else { + $record->set($this->referenceField, $this->reference, false); + } + } + } elseif ($relation instanceof IPF_ORM_Relation_Association) { + + } + } + + public function getReference() + { + return $this->reference; + } + + public function remove($key) + { + $removed = $this->data[$key]; + + unset($this->data[$key]); + return $removed; + } + + public function contains($key) + { + return isset($this->data[$key]); + } + + public function search(IPF_ORM_Record $record) + { + return array_search($record, $this->data, true); + } + + public function get($key) + { + if ( ! isset($this->data[$key])) { + $record = $this->_table->create(); + + if (isset($this->referenceField)) { + $value = $this->reference->get($this->relation->getLocalFieldName()); + + if ($value !== null) { + $record->set($this->referenceField, $value, false); + } else { + $record->set($this->referenceField, $this->reference, false); + } + } + if ($key === null) { + $this->data[] = $record; + } else { + $this->data[$key] = $record; + } + + if (isset($this->keyColumn)) { + $record->set($this->keyColumn, $key); + } + + return $record; + } + + return $this->data[$key]; + } + + public function getPrimaryKeys() + { + $list = array(); + $name = $this->_table->getIdentifier(); + + foreach ($this->data as $record) { + if (is_array($record) && isset($record[$name])) { + $list[] = $record[$name]; + } else { + $list[] = $record->getIncremented(); + } + } + return $list; + } + + public function getKeys() + { + return array_keys($this->data); + } + + public function count() + { + return count($this->data); + } + + public function set($key, $record) + { + if (isset($this->referenceField)) { + $record->set($this->referenceField, $this->reference, false); + } + + $this->data[$key] = $record; + } + + public function add($record, $key = null) + { + if (isset($this->referenceField)) { + $value = $this->reference->get($this->relation->getLocalFieldName()); + + if ($value !== null) { + $record->set($this->referenceField, $value, false); + } else { + $record->set($this->referenceField, $this->reference, false); + } + } + /** + * for some weird reason in_array cannot be used here (php bug ?) + * + * if used it results in fatal error : [ nesting level too deep ] + */ + foreach ($this->data as $val) { + if ($val === $record) { + return false; + } + } + + if (isset($key)) { + if (isset($this->data[$key])) { + return false; + } + $this->data[$key] = $record; + return true; + } + + if (isset($this->keyColumn)) { + $value = $record->get($this->keyColumn); + if ($value === null) { + throw new IPF_ORM_Exception("Couldn't create collection index. Record field '".$this->keyColumn."' was null."); + } + $this->data[$value] = $record; + } else { + $this->data[] = $record; + } + + return true; + } + + public function merge(IPF_ORM_Collection $coll) + { + $localBase = $this->getTable()->getComponentName(); + $otherBase = $coll->getTable()->getComponentName(); + + if ($otherBase != $localBase && !is_subclass_of($otherBase, $localBase) ) { + throw new IPF_ORM_Exception("Can't merge collections with incompatible record types"); + } + + foreach ($coll->getData() as $record) { + $this->add($record); + } + + return $this; + } + + public function loadRelated($name = null) + { + $list = array(); + $query = new IPF_ORM_Query($this->_table->getConnection()); + + if ( ! isset($name)) { + foreach ($this->data as $record) { + $value = $record->getIncremented(); + if ($value !== null) { + $list[] = $value; + } + } + $query->from($this->_table->getComponentName()); + $query->where($this->_table->getComponentName() . '.id IN (' . substr(str_repeat("?, ", count($list)),0,-2) . ')'); + + return $query; + } + + $rel = $this->_table->getRelation($name); + + if ($rel instanceof IPF_ORM_Relation_LocalKey || $rel instanceof IPF_ORM_Relation_ForeignKey) { + foreach ($this->data as $record) { + $list[] = $record[$rel->getLocal()]; + } + } else { + foreach ($this->data as $record) { + $value = $record->getIncremented(); + if ($value !== null) { + $list[] = $value; + } + } + } + + $dql = $rel->getRelationDql(count($list), 'collection'); + + $coll = $query->query($dql, $list); + + $this->populateRelated($name, $coll); + } + + public function populateRelated($name, IPF_ORM_Collection $coll) + { + $rel = $this->_table->getRelation($name); + $table = $rel->getTable(); + $foreign = $rel->getForeign(); + $local = $rel->getLocal(); + + if ($rel instanceof IPF_ORM_Relation_LocalKey) { + foreach ($this->data as $key => $record) { + foreach ($coll as $k => $related) { + if ($related[$foreign] == $record[$local]) { + $this->data[$key]->setRelated($name, $related); + } + } + } + } elseif ($rel instanceof IPF_ORM_Relation_ForeignKey) { + foreach ($this->data as $key => $record) { + if ( ! $record->exists()) { + continue; + } + $sub = new IPF_ORM_Collection($table); + + foreach ($coll as $k => $related) { + if ($related[$foreign] == $record[$local]) { + $sub->add($related); + $coll->remove($k); + } + } + + $this->data[$key]->setRelated($name, $sub); + } + } elseif ($rel instanceof IPF_ORM_Relation_Association) { + $identifier = $this->_table->getIdentifier(); + $asf = $rel->getAssociationFactory(); + $name = $table->getComponentName(); + + foreach ($this->data as $key => $record) { + if ( ! $record->exists()) { + continue; + } + $sub = new IPF_ORM_Collection($table); + foreach ($coll as $k => $related) { + if ($related->get($local) == $record[$identifier]) { + $sub->add($related->get($name)); + } + } + $this->data[$key]->setRelated($name, $sub); + + } + } + } + + public function getNormalIterator() + { + return new IPF_ORM_Collection_Iterator_Normal($this); + } + + public function takeSnapshot() + { + $this->_snapshot = $this->data; + + return $this; + } + + public function getSnapshot() + { + return $this->_snapshot; + } + + public function processDiff() + { + foreach (array_udiff($this->_snapshot, $this->data, array($this, "compareRecords")) as $record) { + $record->delete(); + } + + return $this; + } + + public function toArray($deep = false, $prefixKey = false) + { + $data = array(); + foreach ($this as $key => $record) { + + $key = $prefixKey ? get_class($record) . '_' .$key:$key; + + $data[$key] = $record->toArray($deep, $prefixKey); + } + + return $data; + } + + public function fromArray($array, $deep = true) + { + $data = array(); + foreach ($array as $rowKey => $row) { + $this[$rowKey]->fromArray($row, $deep); + } + } + + public function synchronizeWithArray(array $array) + { + foreach ($this as $key => $record) { + if (isset($array[$key])) { + $record->synchronizeWithArray($array[$key]); + unset($array[$key]); + } else { + // remove records that don't exist in the array + $this->remove($key); + } + } + // create new records for each new row in the array + foreach ($array as $rowKey => $row) { + $this[$rowKey]->fromArray($row); + } + } + public function synchronizeFromArray(array $array) + { + return $this->synchronizeWithArray($array); + } + + public function exportTo($type, $deep = false) + { + if ($type == 'array') { + return $this->toArray($deep); + } else { + return IPF_ORM_Parser::dump($this->toArray($deep, true), $type); + } + } + + public function importFrom($type, $data) + { + if ($type == 'array') { + return $this->fromArray($data); + } else { + return $this->fromArray(IPF_ORM_Parser::load($data, $type)); + } + } + + public function getDeleteDiff() + { + return array_udiff($this->_snapshot, $this->data, array($this, 'compareRecords')); + } + + public function getInsertDiff() + { + return array_udiff($this->data, $this->_snapshot, array($this, "compareRecords")); + } + + protected function compareRecords($a, $b) + { + if ($a->getOid() == $b->getOid()) { + return 0; + } + + return ($a->getOid() > $b->getOid()) ? 1 : -1; + } + + public function save(IPF_ORM_Connection $conn = null) + { + if ($conn == null) { + $conn = $this->_table->getConnection(); + } + + $conn->beginInternalTransaction(); + + $conn->transaction->addCollection($this); + + $this->processDiff(); + + foreach ($this->getData() as $key => $record) { + $record->save($conn); + } + + $conn->commit(); + + return $this; + } + + public function delete(IPF_ORM_Connection $conn = null, $clearColl = true) + { + if ($conn == null) { + $conn = $this->_table->getConnection(); + } + + $conn->beginInternalTransaction(); + $conn->transaction->addCollection($this); + + foreach ($this as $key => $record) { + $record->delete($conn); + } + + $conn->commit(); + + if ($clearColl) { + $this->clear(); + } + + return $this; + } + + public function clear() + { + $this->data = array(); + } + + public function free($deep = false) + { + foreach ($this->getData() as $key => $record) { + if ( ! ($record instanceof IPF_ORM_Null)) { + $record->free($deep); + } + } + + $this->data = array(); + + if ($this->reference) { + $this->reference->free($deep); + $this->reference = null; + } + } + + public function getIterator() + { + $data = $this->data; + return new ArrayIterator($data); + } + + public function __toString() + { + return IPF_ORM_Utils::getCollectionAsString($this); + } + + public function getRelation() + { + return $this->relation; + } +} diff --git a/ipf/orm/configurable.php b/ipf/orm/configurable.php new file mode 100644 index 0000000..3e9f8d6 --- /dev/null +++ b/ipf/orm/configurable.php @@ -0,0 +1,310 @@ +getAttributeFromString($attribute); + $this->_state = $attribute; + } + + if (is_string($value) && isset($stringAttribute)) { + $value = $this->getAttributeValueFromString($stringAttribute, $value); + } + + switch ($attribute) { + case IPF_ORM::ATTR_LISTENER: + $this->setEventListener($value); + break; + case IPF_ORM::ATTR_COLL_KEY: + if ( ! ($this instanceof IPF_ORM_Table)) { + throw new IPF_ORM_Exception("This attribute can only be set at table level."); + } + if ($value !== null && ! $this->hasField($value)) { + throw new IPF_ORM_Exception("Couldn't set collection key attribute. No such field '$value'."); + } + break; + case IPF_ORM::ATTR_CACHE: + case IPF_ORM::ATTR_RESULT_CACHE: + case IPF_ORM::ATTR_QUERY_CACHE: + if ($value !== null) { + if ( ! ($value instanceof IPF_ORM_Cache_Interface)) { + throw new IPF_ORM_Exception('Cache driver should implement IPF_ORM_Cache_Interface'); + } + } + break; + case IPF_ORM::ATTR_VALIDATE: + case IPF_ORM::ATTR_QUERY_LIMIT: + case IPF_ORM::ATTR_QUOTE_IDENTIFIER: + case IPF_ORM::ATTR_PORTABILITY: + case IPF_ORM::ATTR_DEFAULT_TABLE_TYPE: + case IPF_ORM::ATTR_EMULATE_DATABASE: + case IPF_ORM::ATTR_USE_NATIVE_ENUM: + case IPF_ORM::ATTR_DEFAULT_SEQUENCE: + case IPF_ORM::ATTR_EXPORT: + case IPF_ORM::ATTR_DECIMAL_PLACES: + case IPF_ORM::ATTR_LOAD_REFERENCES: + case IPF_ORM::ATTR_RECORD_LISTENER: + case IPF_ORM::ATTR_THROW_EXCEPTIONS: + case IPF_ORM::ATTR_DEFAULT_PARAM_NAMESPACE: + case IPF_ORM::ATTR_AUTOLOAD_TABLE_CLASSES: + case IPF_ORM::ATTR_MODEL_LOADING: + case IPF_ORM::ATTR_RESULT_CACHE_LIFESPAN: + case IPF_ORM::ATTR_QUERY_CACHE_LIFESPAN: + case IPF_ORM::ATTR_RECURSIVE_MERGE_FIXTURES; + case IPF_ORM::ATTR_SINGULARIZE_IMPORT; + case IPF_ORM::ATTR_USE_DQL_CALLBACKS; + + break; + case IPF_ORM::ATTR_SEQCOL_NAME: + if ( ! is_string($value)) { + throw new IPF_ORM_Exception('Sequence column name attribute only accepts string values'); + } + break; + case IPF_ORM::ATTR_FIELD_CASE: + if ($value != 0 && $value != CASE_LOWER && $value != CASE_UPPER) + throw new IPF_ORM_Exception('Field case attribute should be either 0, CASE_LOWER or CASE_UPPER constant.'); + break; + case IPF_ORM::ATTR_SEQNAME_FORMAT: + case IPF_ORM::ATTR_IDXNAME_FORMAT: + case IPF_ORM::ATTR_TBLNAME_FORMAT: + if ($this instanceof IPF_ORM_Table) { + throw new IPF_ORM_Exception('Sequence / index name format attributes cannot be set' + . 'at table level (only at connection or global level).'); + } + break; + default: + throw new IPF_ORM_Exception("Unknown attribute."); + } + + $this->attributes[$attribute] = $value; + } + + public function getParams($namespace = null) + { + if ($namespace == null) { + $namespace = $this->getAttribute(IPF_ORM::ATTR_DEFAULT_PARAM_NAMESPACE); + } + + if ( ! isset($this->_params[$namespace])) { + return null; + } + + return $this->_params[$namespace]; + } + + public function getParamNamespaces() + { + return array_keys($this->_params); + } + + public function setParam($name, $value, $namespace = null) + { + if ($namespace == null) { + $namespace = $this->getAttribute(IPF_ORM::ATTR_DEFAULT_PARAM_NAMESPACE); + } + + $this->_params[$namespace][$name] = $value; + + return $this; + } + + public function getParam($name, $namespace = null) + { + if ($namespace == null) { + $namespace = $this->getAttribute(IPF_ORM::ATTR_DEFAULT_PARAM_NAMESPACE); + } + + if ( ! isset($this->_params[$name])) { + if (isset($this->parent)) { + return $this->parent->getParam($name, $namespace); + } + return null; + } + + return $this->_params[$namespace][$name]; + } + + public function setImpl($template, $class) + { + $this->_impl[$template] = $class; + + return $this; + } + + public function getImpl($template) + { + if ( ! isset($this->_impl[$template])) { + if (isset($this->parent)) { + return $this->parent->getImpl($template); + } + return null; + } + return $this->_impl[$template]; + } + + + public function hasImpl($template) + { + if ( ! isset($this->_impl[$template])) { + if (isset($this->parent)) { + return $this->parent->hasImpl($template); + } + return false; + } + return true; + } + + public function setEventListener($listener) + { + return $this->setListener($listener); + } + + public function addRecordListener($listener, $name = null) + { + if ( ! isset($this->attributes[IPF_ORM::ATTR_RECORD_LISTENER]) || + ! ($this->attributes[IPF_ORM::ATTR_RECORD_LISTENER] instanceof IPF_ORM_Record_Listener_Chain)) { + + $this->attributes[IPF_ORM::ATTR_RECORD_LISTENER] = new IPF_ORM_Record_Listener_Chain(); + } + $this->attributes[IPF_ORM::ATTR_RECORD_LISTENER]->add($listener, $name); + + return $this; + } + + public function getRecordListener() + { + if ( ! isset($this->attributes[IPF_ORM::ATTR_RECORD_LISTENER])) { + if (isset($this->parent)) { + return $this->parent->getRecordListener(); + } + return null; + } + return $this->attributes[IPF_ORM::ATTR_RECORD_LISTENER]; + } + + public function setRecordListener($listener) + { + if ( ! ($listener instanceof IPF_ORM_Record_Listener_Interface) + && ! ($listener instanceof IPF_ORM_Overloadable) + ) { + throw new IPF_ORM_Exception("Couldn't set eventlistener. Record listeners should implement either IPF_ORM_Record_Listener_Interface or IPF_ORM_Overloadable"); + } + $this->attributes[IPF_ORM::ATTR_RECORD_LISTENER] = $listener; + + return $this; + } + + public function addListener($listener, $name = null) + { + if ( ! isset($this->attributes[IPF_ORM::ATTR_LISTENER]) || + ! ($this->attributes[IPF_ORM::ATTR_LISTENER] instanceof IPF_ORM_EventListener_Chain)) { + + $this->attributes[IPF_ORM::ATTR_LISTENER] = new IPF_ORM_EventListener_Chain(); + } + $this->attributes[IPF_ORM::ATTR_LISTENER]->add($listener, $name); + + return $this; + } + + public function getListener() + { + if ( ! isset($this->attributes[IPF_ORM::ATTR_LISTENER])) { + if (isset($this->parent)) { + return $this->parent->getListener(); + } + return null; + } + return $this->attributes[IPF_ORM::ATTR_LISTENER]; + } + + public function setListener($listener) + { + if ( ! ($listener instanceof IPF_ORM_EventListener_Interface) + && ! ($listener instanceof IPF_ORM_Overloadable) + ) { + throw new IPF_ORM_Exception("Couldn't set eventlistener. EventListeners should implement either IPF_ORM_EventListener_Interface or IPF_ORM_Overloadable"); + } + $this->attributes[IPF_ORM::ATTR_LISTENER] = $listener; + + return $this; + } + + public function getAttribute($attribute) + { + if (is_string($attribute)) { + $upper = strtoupper($attribute); + + $const = 'IPF_ORM::ATTR_' . $upper; + + if (defined($const)) { + $attribute = constant($const); + $this->_state = $attribute; + } else { + throw new IPF_ORM_Exception('Unknown attribute: "' . $attribute . '"'); + } + } + + $attribute = (int) $attribute; + + if ($attribute < 0) { + throw new IPF_ORM_Exception('Unknown attribute.'); + } + + if (isset($this->attributes[$attribute])) { + return $this->attributes[$attribute]; + } + + if (isset($this->parent)) { + return $this->parent->getAttribute($attribute); + } + return null; + } + + public function getAttributes() + { + return $this->attributes; + } + + public function setParent(IPF_ORM_Configurable $component) + { + $this->parent = $component; + } + + public function getParent() + { + return $this->parent; + } +} diff --git a/ipf/orm/connection.php b/ipf/orm/connection.php new file mode 100644 index 0000000..6575a4f --- /dev/null +++ b/ipf/orm/connection.php @@ -0,0 +1,848 @@ + false, + 'expression' => false, + 'dataDict' => false, + 'export' => false, + 'import' => false, + 'sequence' => false, + 'unitOfWork' => false, + 'formatter' => false, + 'util' => false, + ); + + protected $properties = array('sql_comments' => array(array('start' => '--', 'end' => "\n", 'escape' => false), + array('start' => '/*', 'end' => '*/', 'escape' => false)), + 'identifier_quoting' => array('start' => '"', 'end' => '"','escape' => '"'), + 'string_quoting' => array('start' => "'", + 'end' => "'", + 'escape' => false, + 'escape_pattern' => false), + 'wildcards' => array('%', '_'), + 'varchar_max_length' => 255, + ); + + protected $serverInfo = array(); + protected $options = array(); + private static $availableDrivers = array( + 'Mysql', + 'Pgsql', + 'Oracle', + 'Informix', + 'Mssql', + 'Sqlite', + 'Firebird' + ); + protected $_count = 0; + + public function __construct(IPF_ORM_Manager $manager, $adapter, $user = null, $pass = null) + { + if (is_object($adapter)) { + if ( ! ($adapter instanceof PDO) && ! in_array('IPF_ORM_Adapter_Interface', class_implements($adapter))) { + throw new IPF_ORM_Exception('First argument should be an instance of PDO or implement IPF_ORM_Adapter_Interface'); + } + $this->dbh = $adapter; + + $this->isConnected = true; + + } else if (is_array($adapter)) { + $this->pendingAttributes[IPF_ORM::ATTR_DRIVER_NAME] = $adapter['scheme']; + + $this->options['dsn'] = $adapter['dsn']; + $this->options['username'] = $adapter['user']; + $this->options['password'] = $adapter['pass']; + + $this->options['other'] = array(); + if (isset($adapter['other'])) { + $this->options['other'] = array(IPF_ORM::ATTR_PERSISTENT => $adapter['persistent']); + } + } + + $this->setParent($manager); + + $this->setAttribute(IPF_ORM::ATTR_CASE, IPF_ORM::CASE_NATURAL); + $this->setAttribute(IPF_ORM::ATTR_ERRMODE, IPF_ORM::ERRMODE_EXCEPTION); + + $this->getAttribute(IPF_ORM::ATTR_LISTENER)->onOpen($this); + } + + public function getOptions() + { + return $this->options; + } + + public function getOption($option) + { + if (isset($this->options[$option])) { + return $this->options[$option]; + } + } + + public function setOption($option, $value) + { + return $this->options[$option] = $value; + } + + public function getAttribute($attribute) + { + if (is_string($attribute)) { + $stringAttribute = $attribute; + $attribute = $this->getAttributeFromString($attribute); + } + + if ($attribute >= 100) { + if ( ! isset($this->attributes[$attribute])) { + return parent::getAttribute($attribute); + } + return $this->attributes[$attribute]; + } + + if ($this->isConnected) { + try { + return $this->dbh->getAttribute($attribute); + } catch (Exception $e) { + throw new IPF_ORM_Exception('Attribute ' . $attribute . ' not found.'); + } + } else { + if ( ! isset($this->pendingAttributes[$attribute])) { + $this->connect(); + $this->getAttribute($attribute); + } + + return $this->pendingAttributes[$attribute]; + } + } + + public static function getAvailableDrivers() + { + return PDO::getAvailableDrivers(); + } + + public function setAttribute($attribute, $value) + { + if (is_string($attribute)) { + $attributeString = $attribute; + $attribute = parent::getAttributeFromString($attribute); + } + + if (is_string($value) && isset($attributeString)) { + $value = parent::getAttributeValueFromString($attributeString, $value); + } + + if ($attribute >= 100) { + parent::setAttribute($attribute, $value); + } else { + if ($this->isConnected) { + $this->dbh->setAttribute($attribute, $value); + } else { + $this->pendingAttributes[$attribute] = $value; + } + } + + return $this; + } + + public function getName() + { + return $this->_name; + } + + public function setName($name) + { + $this->_name = $name; + } + + public function getDriverName() + { + return $this->driverName; + } + + public function __get($name) + { + if (isset($this->properties[$name])) { + return $this->properties[$name]; + } + + if ( ! isset($this->modules[$name])) { + throw new IPF_ORM_Exception('Unknown module / property ' . $name); + } + if ($this->modules[$name] === false) { + switch ($name) { + case 'unitOfWork': + $this->modules[$name] = new IPF_ORM_Connection_UnitOfWork($this); + break; + case 'formatter': + $this->modules[$name] = new IPF_ORM_Formatter($this); + break; + default: + $class = 'IPF_ORM_' . ucwords($name) . '_' . $this->getDriverName(); + $this->modules[$name] = new $class($this); + } + } + + return $this->modules[$name]; + } + + public function getManager() + { + return $this->getParent(); + } + + public function getDbh() + { + $this->connect(); + + return $this->dbh; + } + + public function connect() + { + if ($this->isConnected) { + return false; + } + + $event = new IPF_ORM_Event($this, IPF_ORM_Event::CONN_CONNECT); + + $this->getListener()->preConnect($event); + + $e = explode(':', $this->options['dsn']); + $found = false; + + if (extension_loaded('pdo')) { + if (in_array($e[0], PDO::getAvailableDrivers())) { + try { + $this->dbh = new PDO($this->options['dsn'], $this->options['username'], + (!$this->options['password'] ? '':$this->options['password']), $this->options['other']); + + $this->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } catch (PDOException $e) { + throw new IPF_ORM_Exception('PDO Connection Error: ' . $e->getMessage()); + } + $found = true; + } + } + + if ( !$found) { + throw new IPF_Exception_Panic("Couldn't locate driver named " . $e[0]); + } + + // attach the pending attributes to adapter + foreach($this->pendingAttributes as $attr => $value) { + // some drivers don't support setting this so we just skip it + if ($attr == IPF_ORM::ATTR_DRIVER_NAME) { + continue; + } + $this->dbh->setAttribute($attr, $value); + } + + $this->isConnected = true; + + $this->getListener()->postConnect($event); + return true; + } + + public function incrementQueryCount() + { + $this->_count++; + } + + public function driverName($name) + { + } + + public function supports($feature) + { + return (isset($this->supported[$feature]) + && ($this->supported[$feature] === 'emulated' + || $this->supported[$feature])); + } + + public function replace(IPF_ORM_Table $table, array $fields, array $keys) + { + if (empty($keys)) { + throw new IPF_ORM_Exception('Not specified which fields are keys'); + } + $identifier = (array) $table->getIdentifier(); + $condition = array(); + + foreach ($fields as $fieldName => $value) { + if (in_array($fieldName, $keys)) { + if ($value !== null) { + $condition[] = $table->getColumnName($fieldName) . ' = ?'; + $conditionValues[] = $value; + } + } + } + + $affectedRows = 0; + if ( ! empty($condition) && ! empty($conditionValues)) { + $query = 'DELETE FROM ' . $this->quoteIdentifier($table->getTableName()) + . ' WHERE ' . implode(' AND ', $condition); + + $affectedRows = $this->exec($query, $conditionValues); + } + + $this->insert($table, $fields); + + $affectedRows++; + + return $affectedRows; + } + + public function delete(IPF_ORM_Table $table, array $identifier) + { + $tmp = array(); + + foreach (array_keys($identifier) as $id) { + $tmp[] = $this->quoteIdentifier($table->getColumnName($id)) . ' = ?'; + } + + $query = 'DELETE FROM ' + . $this->quoteIdentifier($table->getTableName()) + . ' WHERE ' . implode(' AND ', $tmp); + + return $this->exec($query, array_values($identifier)); + } + + public function update(IPF_ORM_Table $table, array $fields, array $identifier) + { + if (empty($fields)) { + return false; + } + + $set = array(); + foreach ($fields as $fieldName => $value) { + if ($value instanceof IPF_ORM_Expression) { + $set[] = $this->quoteIdentifier($table->getColumnName($fieldName)) . ' = ' . $value->getSql(); + unset($fields[$fieldName]); + } else { + $set[] = $this->quoteIdentifier($table->getColumnName($fieldName)) . ' = ?'; + } + } + + $params = array_merge(array_values($fields), array_values($identifier)); + + $sql = 'UPDATE ' . $this->quoteIdentifier($table->getTableName()) + . ' SET ' . implode(', ', $set) + . ' WHERE ' . implode(' = ? AND ', $table->getIdentifierColumnNames()) + . ' = ?'; + + return $this->exec($sql, $params); + } + + public function insert(IPF_ORM_Table $table, array $fields) + { + $tableName = $table->getTableName(); + + // column names are specified as array keys + $cols = array(); + // the query VALUES will contain either expresions (eg 'NOW()') or ? + $a = array(); + foreach ($fields as $fieldName => $value) { + $cols[] = $this->quoteIdentifier($table->getColumnName($fieldName)); + if ($value instanceof IPF_ORM_Expression) { + $a[] = $value->getSql(); + unset($fields[$fieldName]); + } else { + $a[] = '?'; + } + } + + // build the statement + $query = 'INSERT INTO ' . $this->quoteIdentifier($tableName) + . ' (' . implode(', ', $cols) . ')' + . ' VALUES (' . implode(', ', $a) . ')'; + + return $this->exec($query, array_values($fields)); + } + + public function setCharset($charset) + { + } + + public function quoteIdentifier($str, $checkOption = true) + { + // quick fix for the identifiers that contain a dot + if (strpos($str, '.')) { + $e = explode('.', $str); + + return $this->formatter->quoteIdentifier($e[0], $checkOption) . '.' + . $this->formatter->quoteIdentifier($e[1], $checkOption); + } + return $this->formatter->quoteIdentifier($str, $checkOption); + } + + public function quoteMultipleIdentifier($arr, $checkOption = true) + { + foreach ($arr as $k => $v) { + $arr[$k] = $this->quoteIdentifier($v, $checkOption); + } + + return $arr; + } + + public function convertBooleans($item) + { + return $this->formatter->convertBooleans($item); + } + + public function quote($input, $type = null) + { + return $this->formatter->quote($input, $type); + } + + public function setDateFormat($format = null) + { + } + + public function fetchAll($statement, array $params = array()) + { + return $this->execute($statement, $params)->fetchAll(IPF_ORM::FETCH_ASSOC); + } + + public function fetchOne($statement, array $params = array(), $colnum = 0) + { + return $this->execute($statement, $params)->fetchColumn($colnum); + } + + public function fetchRow($statement, array $params = array()) + { + return $this->execute($statement, $params)->fetch(IPF_ORM::FETCH_ASSOC); + } + + public function fetchArray($statement, array $params = array()) + { + return $this->execute($statement, $params)->fetch(IPF_ORM::FETCH_NUM); + } + + public function fetchColumn($statement, array $params = array(), $colnum = 0) + { + return $this->execute($statement, $params)->fetchAll(IPF_ORM::FETCH_COLUMN, $colnum); + } + + public function fetchAssoc($statement, array $params = array()) + { + return $this->execute($statement, $params)->fetchAll(IPF_ORM::FETCH_ASSOC); + } + + public function fetchBoth($statement, array $params = array()) + { + return $this->execute($statement, $params)->fetchAll(IPF_ORM::FETCH_BOTH); + } + + public function query($query, array $params = array(), $hydrationMode = null) + { + $parser = new IPF_ORM_Query($this); + $res = $parser->query($query, $params, $hydrationMode); + $parser->free(); + + return $res; + } + + public function prepare($statement) + { + $this->connect(); + + try { + $event = new IPF_ORM_Event($this, IPF_ORM_Event::CONN_PREPARE, $statement); + + $this->getAttribute(IPF_ORM::ATTR_LISTENER)->prePrepare($event); + + $stmt = false; + + if ( ! $event->skipOperation) { + $stmt = $this->dbh->prepare($statement); + } + + $this->getAttribute(IPF_ORM::ATTR_LISTENER)->postPrepare($event); + + return new IPF_ORM_Connection_Statement($this, $stmt); + } catch(IPF_ORM_Exception_Adapter $e) { + } catch(PDOException $e) { } + + $this->rethrowException($e, $this); + } + + public function queryOne($query, array $params = array()) + { + $parser = new IPF_ORM_Query($this); + + $coll = $parser->query($query, $params); + if ( ! $coll->contains(0)) { + return false; + } + return $coll[0]; + } + + public function select($query, $limit = 0, $offset = 0) + { + if ($limit > 0 || $offset > 0) { + $query = $this->modifyLimitQuery($query, $limit, $offset); + } + return $this->execute($query); + } + + public function standaloneQuery($query, $params = array()) + { + return $this->execute($query, $params); + } + + public function execute($query, array $params = array()) + { + $this->connect(); + + try { + if ( ! empty($params)) { + $stmt = $this->prepare($query); + $stmt->execute($params); + + return $stmt; + } else { + $event = new IPF_ORM_Event($this, IPF_ORM_Event::CONN_QUERY, $query, $params); + + $this->getAttribute(IPF_ORM::ATTR_LISTENER)->preQuery($event); + + if ( ! $event->skipOperation) { + $stmt = $this->dbh->query($query); + $this->_count++; + } + $this->getAttribute(IPF_ORM::ATTR_LISTENER)->postQuery($event); + + return $stmt; + } + } catch (IPF_ORM_Exception_Adapter $e) { + } catch (PDOException $e) { } + + $this->rethrowException($e, $this); + } + + public function exec($query, array $params = array()) + { + $this->connect(); + + try { + if ( ! empty($params)) { + $stmt = $this->prepare($query); + $stmt->execute($params); + + return $stmt->rowCount(); + } else { + $event = new IPF_ORM_Event($this, IPF_ORM_Event::CONN_EXEC, $query, $params); + + $this->getAttribute(IPF_ORM::ATTR_LISTENER)->preExec($event); + if ( ! $event->skipOperation) { + $count = $this->dbh->exec($query); + + $this->_count++; + } + $this->getAttribute(IPF_ORM::ATTR_LISTENER)->postExec($event); + + return $count; + } + } catch (IPF_ORM_Exception_Adapter $e) { + } catch (PDOException $e) { } + + $this->rethrowException($e, $this); + } + + public function rethrowException(Exception $e, $invoker) + { + $event = new IPF_ORM_Event($this, IPF_ORM_Event::CONN_ERROR); + + $this->getListener()->preError($event); + + $name = 'IPF_ORM_Exception_' . $this->driverName; + + $exc = new $name($e->getMessage(), (int) $e->getCode()); + if ( ! is_array($e->errorInfo)) { + $e->errorInfo = array(null, null, null, null); + } + $exc->processErrorInfo($e->errorInfo); + + if ($this->getAttribute(IPF_ORM::ATTR_THROW_EXCEPTIONS)) { + throw $exc; + } + + $this->getListener()->postError($event); + } + + public function hasTable($name) + { + return isset($this->tables[$name]); + } + + public function getTable($name) + { + if (isset($this->tables[$name])) { + return $this->tables[$name]; + } + $class = $name . 'Table'; + + if (class_exists($class, $this->getAttribute(IPF_ORM::ATTR_AUTOLOAD_TABLE_CLASSES)) && + in_array('IPF_ORM_Table', class_parents($class))) { + $table = new $class($name, $this, true); + } else { + $table = new IPF_ORM_Table($name, $this, true); + } + + $this->tables[$name] = $table; + + return $table; + } + + public function getTables() + { + return $this->tables; + } + + public function getIterator() + { + return new ArrayIterator($this->tables); + } + + public function count() + { + return $this->_count; + } + + public function addTable(IPF_ORM_Table $table) + { + $name = $table->getComponentName(); + + if (isset($this->tables[$name])) { + return false; + } + $this->tables[$name] = $table; + return true; + } + + public function create($name) + { + return $this->getTable($name)->create(); + } + + public function createQuery() + { + return new IPF_ORM_Query($this); + } + + public function flush() + { + $this->beginInternalTransaction(); + $this->unitOfWork->saveAll(); + $this->commit(); + } + + public function clear() + { + foreach ($this->tables as $k => $table) { + $table->getRepository()->evictAll(); + $table->clear(); + } + } + + public function evictTables() + { + $this->tables = array(); + $this->exported = array(); + } + + public function close() + { + $event = new IPF_ORM_Event($this, IPF_ORM_Event::CONN_CLOSE); + + $this->getAttribute(IPF_ORM::ATTR_LISTENER)->preClose($event); + + $this->clear(); + + unset($this->dbh); + $this->isConnected = false; + + $this->getAttribute(IPF_ORM::ATTR_LISTENER)->postClose($event); + } + + public function getTransactionLevel() + { + return $this->transaction->getTransactionLevel(); + } + + public function errorCode() + { + $this->connect(); + + return $this->dbh->errorCode(); + } + + public function errorInfo() + { + $this->connect(); + + return $this->dbh->errorInfo(); + } + + public function getCacheDriver() + { + return $this->getResultCacheDriver(); + } + + public function getResultCacheDriver() + { + if ( ! $this->getAttribute(IPF_ORM::ATTR_RESULT_CACHE)) { + throw new IPF_ORM_Exception('Result Cache driver not initialized.'); + } + + return $this->getAttribute(IPF_ORM::ATTR_RESULT_CACHE); + } + + public function getQueryCacheDriver() + { + if ( ! $this->getAttribute(IPF_ORM::ATTR_QUERY_CACHE)) { + throw new IPF_ORM_Exception('Query Cache driver not initialized.'); + } + + return $this->getAttribute(IPF_ORM::ATTR_QUERY_CACHE); + } + + public function lastInsertId($table = null, $field = null) + { + return $this->sequence->lastInsertId($table, $field); + } + + public function beginTransaction($savepoint = null) + { + return $this->transaction->beginTransaction($savepoint); + } + + public function beginInternalTransaction($savepoint = null) + { + return $this->transaction->beginInternalTransaction($savepoint); + } + + public function commit($savepoint = null) + { + return $this->transaction->commit($savepoint); + } + + public function rollback($savepoint = null) + { + $this->transaction->rollback($savepoint); + } + + public function createDatabase() + { + if ( ! $dsn = $this->getOption('dsn')) { + throw new IPF_ORM_Exception('You must create your IPF_ORM_Connection by using a valid IPF style dsn in order to use the create/drop database functionality'); + } + + // Parse pdo dsn so we are aware of the connection information parts + $info = $this->getManager()->parsePdoDsn($dsn); + + // Get the temporary connection to issue the drop database command + $tmpConnection = $this->getTmpConnection($info); + + try { + // Issue create database command + $tmpConnection->export->createDatabase($info['dbname']); + } catch (Exception $e) {} + + // Close the temporary connection used to issue the drop database command + $this->getManager()->closeConnection($tmpConnection); + + // Re-create IPF or PDO style dsn + if ($info['unix_socket']) { + $dsn = array($info['scheme'] . ':unix_socket=' . $info['unix_socket'] . ';dbname=' . $info['dbname'], $this->getOption('username'), $this->getOption('password')); + } else { + $dsn = $info['scheme'] . '://' . $this->getOption('username') . ':' . $this->getOption('password') . '@' . $info['host'] . '/' . $info['dbname']; + } + + // Re-open connection with the newly created database + $this->getManager()->openConnection($dsn, $this->getName(), true); + + if (isset($e)) { + return $e; + } else { + return 'Successfully created database for connection "' . $this->getName() . '" named "' . $info['dbname'] . '"'; + } + } + + public function dropDatabase() + { + if ( ! $dsn = $this->getOption('dsn')) { + throw new IPF_ORM_Exception('You must create your IPF_ORM_Connection by using a valid IPF style dsn in order to use the create/drop database functionality'); + } + + // Parse pdo dsn so we are aware of the connection information parts + $info = $this->getManager()->parsePdoDsn($dsn); + + // Get the temporary connection to issue the drop database command + $tmpConnection = $this->getTmpConnection($info); + + try { + // Issue drop database command + $tmpConnection->export->dropDatabase($info['dbname']); + } catch (Exception $e) {} + + // Close the temporary connection used to issue the drop database command + $this->getManager()->closeConnection($tmpConnection); + + // Re-create IPF or PDO style dsn + if ($info['unix_socket']) { + $dsn = array($info['scheme'] . ':unix_socket=' . $info['unix_socket'] . ';dbname=' . $info['dbname'], $this->getOption('username'), $this->getOption('password')); + } else { + $dsn = $info['scheme'] . '://' . $this->getOption('username') . ':' . $this->getOption('password') . '@' . $info['host'] . '/' . $info['dbname']; + } + + // Re-open connection with the newly created database + $this->getManager()->openConnection($dsn, $this->getName(), true); + + if (isset($e)) { + return $e; + } else { + return 'Successfully dropped database for connection "' . $this->getName() . '" named "' . $info['dbname'] . '"'; + } + } + + public function getTmpConnection($info) + { + if ($info['unix_socket']) { + $pdoDsn = $info['scheme'] . ':unix_socket=' . $info['unix_socket']; + } else { + $pdoDsn = $info['scheme'] . ':host=' . $info['host']; + } + + if (isset($this->export->tmpConnectionDatabase) && $this->export->tmpConnectionDatabase) { + $pdoDsn .= ';dbname=' . $this->export->tmpConnectionDatabase; + } + + $username = $this->getOption('username'); + $password = $this->getOption('password'); + + return $this->getManager()->openConnection(new PDO($pdoDsn, $username, $password), 'ipf_tmp_connection', false); + } + + public function modifyLimitQuery($query, $limit = false, $offset = false, $isManip = false) + { + return $query; + } + + public function modifyLimitSubquery(IPF_ORM_Table $rootTable, $query, $limit = false, + $offset = false, $isManip = false) + { + return $this->modifyLimitQuery($query, $limit, $offset, $isManip); + } + + public function __toString() + { + return IPF_ORM_Utils::getConnectionAsString($this); + } +} \ No newline at end of file diff --git a/ipf/orm/connection/module.php b/ipf/orm/connection/module.php new file mode 100644 index 0000000..1a76572 --- /dev/null +++ b/ipf/orm/connection/module.php @@ -0,0 +1,29 @@ +getCurrentConnection(); + } + $this->conn = $conn; + + $e = explode('_', get_class($this)); + + $this->moduleName = $e[1]; + } + + public function getConnection() + { + return $this->conn; + } + + public function getModuleName() + { + return $this->moduleName; + } +} \ No newline at end of file diff --git a/ipf/orm/connection/mysql.php b/ipf/orm/connection/mysql.php new file mode 100644 index 0000000..4177841 --- /dev/null +++ b/ipf/orm/connection/mysql.php @@ -0,0 +1,100 @@ +setAttribute(IPF_ORM::ATTR_DEFAULT_TABLE_TYPE, 'INNODB'); + $this->supported = array( + 'sequences' => 'emulated', + 'indexes' => true, + 'affected_rows' => true, + 'transactions' => true, + 'savepoints' => false, + 'summary_functions' => true, + 'order_by_text' => true, + 'current_id' => 'emulated', + 'limit_queries' => true, + 'LOBs' => true, + 'replace' => true, + 'sub_selects' => true, + 'auto_increment' => true, + 'primary_key' => true, + 'result_introspection' => true, + 'prepared_statements' => 'emulated', + 'identifier_quoting' => true, + 'pattern_escaping' => true + ); + + $this->properties['string_quoting'] = array('start' => "'", + 'end' => "'", + 'escape' => '\\', + 'escape_pattern' => '\\'); + + $this->properties['identifier_quoting'] = array('start' => '`', + 'end' => '`', + 'escape' => '`'); + + $this->properties['sql_comments'] = array( + array('start' => '-- ', 'end' => "\n", 'escape' => false), + array('start' => '#', 'end' => "\n", 'escape' => false), + array('start' => '/*', 'end' => '*/', 'escape' => false), + ); + + $this->properties['varchar_max_length'] = 255; + + parent::__construct($manager, $adapter); + } + + public function connect() + { + $connected = parent::connect(); + $this->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); + return $connected; + } + + public function getDatabaseName() + { + return $this->fetchOne('SELECT DATABASE()'); + } + + public function setCharset($charset) + { + $query = 'SET NAMES ' . $this->quote($charset); + $this->exec($query); + } + + public function replace(IPF_ORM_Table $table, array $fields, array $keys) + { + if (empty($keys)) { + throw new IPF_ORM_Exception('Not specified which fields are keys'); + } + $columns = array(); + $values = array(); + $params = array(); + foreach ($fields as $fieldName => $value) { + $columns[] = $table->getColumnName($fieldName); + $values[] = '?'; + $params[] = $value; + } + $query = 'REPLACE INTO ' . $table->getTableName() . ' (' . implode(',', $columns) . ') VALUES (' . implode(',', $values) . ')'; + return $this->exec($query, $params); + } + + public function modifyLimitQuery($query, $limit = false,$offset = false,$isManip=false) + { + $limit = (int) $limit; + $offset = (int) $offset; + + if ($limit && $offset) { + $query .= ' LIMIT ' . $limit . ' OFFSET ' . $offset; + } elseif ($limit && ! $offset) { + $query .= ' LIMIT ' . $limit; + } elseif ( ! $limit && $offset) { + $query .= ' LIMIT 999999999999 OFFSET ' . $offset; + } + return $query; + } +} \ No newline at end of file diff --git a/ipf/orm/connection/statement.php b/ipf/orm/connection/statement.php new file mode 100644 index 0000000..d29432b --- /dev/null +++ b/ipf/orm/connection/statement.php @@ -0,0 +1,185 @@ +_conn = $conn; + $this->_stmt = $stmt; + + if ($stmt === false) { + throw new IPF_ORM_Exception('Unknown statement object given.'); + } + } + + public function getConnection() + { + return $this->_conn; + } + public function getStatement() + { + return $this->_stmt; + } + public function getQuery() + { + return $this->_stmt->queryString; + } + + public function bindColumn($column, $param, $type = null) + { + if ($type === null) { + return $this->_stmt->bindColumn($column, $param); + } else { + return $this->_stmt->bindColumn($column, $param, $type); + } + } + + public function bindValue($param, $value, $type = null) + { + if ($type === null) { + return $this->_stmt->bindValue($param, $value); + } else { + return $this->_stmt->bindValue($param, $value, $type); + } + } + + public function bindParam($column, &$variable, $type = null, $length = null, $driverOptions = array()) + { + if ($type === null) { + return $this->_stmt->bindParam($column, $variable); + } else { + return $this->_stmt->bindParam($column, $variable, $type, $length, $driverOptions); + } + } + + public function closeCursor() + { + return $this->_stmt->closeCursor(); + } + + public function columnCount() + { + return $this->_stmt->columnCount(); + } + + public function errorCode() + { + return $this->_stmt->errorCode(); + } + + public function errorInfo() + { + return $this->_stmt->errorInfo(); + } + + public function execute($params = null) + { + try { + $event = new IPF_ORM_Event($this, IPF_ORM_Event::STMT_EXECUTE, $this->getQuery(), $params); + $this->_conn->getListener()->preStmtExecute($event); + + $result = true; + if ( ! $event->skipOperation) { + $result = $this->_stmt->execute($params); + $this->_conn->incrementQueryCount(); + } + + $this->_conn->getListener()->postStmtExecute($event); + + return $result; + } catch (PDOException $e) { + } catch (IPF_ORM_Exception_Adapter $e) { + } + + $this->_conn->rethrowException($e, $this); + + return false; + } + + public function fetch($fetchMode = IPF_ORM::FETCH_BOTH, + $cursorOrientation = IPF_ORM::FETCH_ORI_NEXT, + $cursorOffset = null) + { + $event = new IPF_ORM_Event($this, IPF_ORM_Event::STMT_FETCH, $this->getQuery()); + + $event->fetchMode = $fetchMode; + $event->cursorOrientation = $cursorOrientation; + $event->cursorOffset = $cursorOffset; + + $data = $this->_conn->getListener()->preFetch($event); + + if ( ! $event->skipOperation) { + $data = $this->_stmt->fetch($fetchMode, $cursorOrientation, $cursorOffset); + } + + $this->_conn->getListener()->postFetch($event); + + return $data; + } + + public function fetchAll($fetchMode = IPF_ORM::FETCH_BOTH, + $columnIndex = null) + { + $event = new IPF_ORM_Event($this, IPF_ORM_Event::STMT_FETCHALL, $this->getQuery()); + $event->fetchMode = $fetchMode; + $event->columnIndex = $columnIndex; + + $this->_conn->getListener()->preFetchAll($event); + + if ( ! $event->skipOperation) { + if ($columnIndex !== null) { + $data = $this->_stmt->fetchAll($fetchMode, $columnIndex); + } else { + $data = $this->_stmt->fetchAll($fetchMode); + } + + $event->data = $data; + } + + $this->_conn->getListener()->postFetchAll($event); + + return $data; + } + + public function fetchColumn($columnIndex = 0) + { + return $this->_stmt->fetchColumn($columnIndex); + } + + public function fetchObject($className = 'stdClass', $args = array()) + { + return $this->_stmt->fetchObject($className, $args); + } + + public function getAttribute($attribute) + { + return $this->_stmt->getAttribute($attribute); + } + + public function getColumnMeta($column) + { + return $this->_stmt->getColumnMeta($column); + } + + public function nextRowset() + { + return $this->_stmt->nextRowset(); + } + + public function rowCount() + { + return $this->_stmt->rowCount(); + } + + public function setAttribute($attribute, $value) + { + return $this->_stmt->setAttribute($attribute, $value); + } + + public function setFetchMode($mode, $arg1 = null, $arg2 = null) + { + return $this->_stmt->setFetchMode($mode, $arg1, $arg2); + } +} diff --git a/ipf/orm/connection/unitofwork.php b/ipf/orm/connection/unitofwork.php new file mode 100644 index 0000000..abeb5fe --- /dev/null +++ b/ipf/orm/connection/unitofwork.php @@ -0,0 +1,669 @@ +getConnection(); + + $state = $record->state(); + if ($state === IPF_ORM_Record::STATE_LOCKED) { + return false; + } + + $record->state(IPF_ORM_Record::STATE_LOCKED); + + $conn->beginInternalTransaction(); + $saveLater = $this->saveRelated($record); + + $record->state($state); + + if ($record->isValid()) { + $event = new IPF_ORM_Event($record, IPF_ORM_Event::RECORD_SAVE); + $record->preSave($event); + $record->getTable()->getRecordListener()->preSave($event); + $state = $record->state(); + + if ( ! $event->skipOperation) { + switch ($state) { + case IPF_ORM_Record::STATE_TDIRTY: + $this->insert($record); + break; + case IPF_ORM_Record::STATE_DIRTY: + case IPF_ORM_Record::STATE_PROXY: + $this->update($record); + break; + case IPF_ORM_Record::STATE_CLEAN: + case IPF_ORM_Record::STATE_TCLEAN: + break; + } + } + + // NOTE: what about referential integrity issues? + foreach ($record->getPendingDeletes() as $pendingDelete) { + $pendingDelete->delete(); + } + + $record->getTable()->getRecordListener()->postSave($event); + $record->postSave($event); + } else { + $conn->transaction->addInvalid($record); + } + + $state = $record->state(); + + $record->state(IPF_ORM_Record::STATE_LOCKED); + + foreach ($saveLater as $fk) { + $alias = $fk->getAlias(); + + if ($record->hasReference($alias)) { + $obj = $record->$alias; + + // check that the related object is not an instance of IPF_ORM_Null + if ( ! ($obj instanceof IPF_ORM_Null)) { + $obj->save($conn); + } + } + } + + // save the MANY-TO-MANY associations + $this->saveAssociations($record); + + $record->state($state); + + $conn->commit(); + + return true; + } + + public function save(IPF_ORM_Record $record) + { + $event = new IPF_ORM_Event($record, IPF_ORM_Event::RECORD_SAVE); + + $record->preSave($event); + + $record->getTable()->getRecordListener()->preSave($event); + + if ( ! $event->skipOperation) { + switch ($record->state()) { + case IPF_ORM_Record::STATE_TDIRTY: + $this->insert($record); + break; + case IPF_ORM_Record::STATE_DIRTY: + case IPF_ORM_Record::STATE_PROXY: + $this->update($record); + break; + case IPF_ORM_Record::STATE_CLEAN: + case IPF_ORM_Record::STATE_TCLEAN: + // do nothing + break; + } + } + + $record->getTable()->getRecordListener()->postSave($event); + + $record->postSave($event); + } + + public function delete(IPF_ORM_Record $record) + { + $deletions = array(); + $this->_collectDeletions($record, $deletions); + return $this->_executeDeletions($deletions); + } + + private function _collectDeletions(IPF_ORM_Record $record, array &$deletions) + { + if ( ! $record->exists()) { + return; + } + + $deletions[$record->getOid()] = $record; + $this->_cascadeDelete($record, $deletions); + } + + private function _executeDeletions(array $deletions) + { + // collect class names + $classNames = array(); + foreach ($deletions as $record) { + $classNames[] = $record->getTable()->getComponentName(); + } + $classNames = array_unique($classNames); + + // order deletes + $executionOrder = $this->buildFlushTree($classNames); + + // execute + try { + $this->conn->beginInternalTransaction(); + + for ($i = count($executionOrder) - 1; $i >= 0; $i--) { + $className = $executionOrder[$i]; + $table = $this->conn->getTable($className); + + // collect identifiers + $identifierMaps = array(); + $deletedRecords = array(); + foreach ($deletions as $oid => $record) { + if ($record->getTable()->getComponentName() == $className) { + $veto = $this->_preDelete($record); + if ( ! $veto) { + $identifierMaps[] = $record->identifier(); + $deletedRecords[] = $record; + unset($deletions[$oid]); + } + } + } + + if (count($deletedRecords) < 1) { + continue; + } + + // extract query parameters (only the identifier values are of interest) + $params = array(); + $columnNames = array(); + foreach ($identifierMaps as $idMap) { + while (list($fieldName, $value) = each($idMap)) { + $params[] = $value; + $columnNames[] = $table->getColumnName($fieldName); + } + } + $columnNames = array_unique($columnNames); + + // delete + $tableName = $table->getTableName(); + $sql = "DELETE FROM " . $this->conn->quoteIdentifier($tableName) . " WHERE "; + + if ($table->isIdentifierComposite()) { + $sql .= $this->_buildSqlCompositeKeyCondition($columnNames, count($identifierMaps)); + $this->conn->exec($sql, $params); + } else { + $sql .= $this->_buildSqlSingleKeyCondition($columnNames, count($params)); + $this->conn->exec($sql, $params); + } + + // adjust state, remove from identity map and inform postDelete listeners + foreach ($deletedRecords as $record) { + // currently just for bc! + $this->_deleteCTIParents($table, $record); + //-- + $record->state(IPF_ORM_Record::STATE_TCLEAN); + $record->getTable()->removeRecord($record); + $this->_postDelete($record); + } + } + + $this->conn->commit(); + // trigger postDelete for records skipped during the deletion (veto!) + foreach ($deletions as $skippedRecord) { + $this->_postDelete($skippedRecord); + } + + return true; + } catch (Exception $e) { + $this->conn->rollback(); + throw $e; + } + } + + private function _buildSqlSingleKeyCondition($columnNames, $numRecords) + { + $idColumn = $this->conn->quoteIdentifier($columnNames[0]); + return implode(' OR ', array_fill(0, $numRecords, "$idColumn = ?")); + } + + private function _buildSqlCompositeKeyCondition($columnNames, $numRecords) + { + $singleCondition = ""; + foreach ($columnNames as $columnName) { + $columnName = $this->conn->quoteIdentifier($columnName); + if ($singleCondition === "") { + $singleCondition .= "($columnName = ?"; + } else { + $singleCondition .= " AND $columnName = ?"; + } + } + $singleCondition .= ")"; + $fullCondition = implode(' OR ', array_fill(0, $numRecords, $singleCondition)); + + return $fullCondition; + } + + protected function _cascadeDelete(IPF_ORM_Record $record, array &$deletions) + { + foreach ($record->getTable()->getRelations() as $relation) { + if ($relation->isCascadeDelete()) { + $fieldName = $relation->getAlias(); + // if it's a xToOne relation and the related object is already loaded + // we don't need to refresh. + if ( ! ($relation->getType() == IPF_ORM_Relation::ONE && isset($record->$fieldName))) { + $record->refreshRelated($relation->getAlias()); + } + $relatedObjects = $record->get($relation->getAlias()); + if ($relatedObjects instanceof IPF_ORM_Record && $relatedObjects->exists() + && ! isset($deletions[$relatedObjects->getOid()])) { + $this->_collectDeletions($relatedObjects, $deletions); + } else if ($relatedObjects instanceof IPF_ORM_Collection && count($relatedObjects) > 0) { + // cascade the delete to the other objects + foreach ($relatedObjects as $object) { + if ( ! isset($deletions[$object->getOid()])) { + $this->_collectDeletions($object, $deletions); + } + } + } + } + } + } + + public function saveRelated(IPF_ORM_Record $record) + { + $saveLater = array(); + foreach ($record->getReferences() as $k => $v) { + $rel = $record->getTable()->getRelation($k); + + $local = $rel->getLocal(); + $foreign = $rel->getForeign(); + + if ($rel instanceof IPF_ORM_Relation_ForeignKey) { + $saveLater[$k] = $rel; + } else if ($rel instanceof IPF_ORM_Relation_LocalKey) { + // ONE-TO-ONE relationship + $obj = $record->get($rel->getAlias()); + + // Protection against infinite function recursion before attempting to save + if ($obj instanceof IPF_ORM_Record && $obj->isModified()) { + $obj->save($this->conn); + + /** Can this be removed? + $id = array_values($obj->identifier()); + + foreach ((array) $rel->getLocal() as $k => $field) { + $record->set($field, $id[$k]); + } + */ + } + } + } + + return $saveLater; + } + + public function saveAssociations(IPF_ORM_Record $record) + { + foreach ($record->getReferences() as $k => $v) { + $rel = $record->getTable()->getRelation($k); + + if ($rel instanceof IPF_ORM_Relation_Association) { + $v->save($this->conn); + + $assocTable = $rel->getAssociationTable(); + foreach ($v->getDeleteDiff() as $r) { + $query = 'DELETE FROM ' . $assocTable->getTableName() + . ' WHERE ' . $rel->getForeign() . ' = ?' + . ' AND ' . $rel->getLocal() . ' = ?'; + + $this->conn->execute($query, array($r->getIncremented(), $record->getIncremented())); + } + + foreach ($v->getInsertDiff() as $r) { + $assocRecord = $assocTable->create(); + $assocRecord->set($assocTable->getFieldName($rel->getForeign()), $r); + $assocRecord->set($assocTable->getFieldName($rel->getLocal()), $record); + $this->saveGraph($assocRecord); + } + } + } + } + + private function _preDelete(IPF_ORM_Record $record) + { + $event = new IPF_ORM_Event($record, IPF_ORM_Event::RECORD_DELETE); + $record->preDelete($event); + $record->getTable()->getRecordListener()->preDelete($event); + + return $event->skipOperation; + } + + private function _postDelete(IPF_ORM_Record $record) + { + $event = new IPF_ORM_Event($record, IPF_ORM_Event::RECORD_DELETE); + $record->postDelete($event); + $record->getTable()->getRecordListener()->postDelete($event); + } + + public function saveAll() + { + // get the flush tree + $tree = $this->buildFlushTree($this->conn->getTables()); + + // save all records + foreach ($tree as $name) { + $table = $this->conn->getTable($name); + foreach ($table->getRepository() as $record) { + $this->saveGraph($record); + } + } + } + + public function update(IPF_ORM_Record $record) + { + $event = new IPF_ORM_Event($record, IPF_ORM_Event::RECORD_UPDATE); + $record->preUpdate($event); + $table = $record->getTable(); + $table->getRecordListener()->preUpdate($event); + + if ( ! $event->skipOperation) { + $identifier = $record->identifier(); + if ($table->getOption('joinedParents')) { + // currrently just for bc! + $this->_updateCTIRecord($table, $record); + //-- + } else { + $array = $record->getPrepared(); + $this->conn->update($table, $array, $identifier); + } + $record->assignIdentifier(true); + } + + $table->getRecordListener()->postUpdate($event); + + $record->postUpdate($event); + + return true; + } + + public function insert(IPF_ORM_Record $record) + { + // listen the onPreInsert event + $event = new IPF_ORM_Event($record, IPF_ORM_Event::RECORD_INSERT); + $record->preInsert($event); + $table = $record->getTable(); + $table->getRecordListener()->preInsert($event); + + if ( ! $event->skipOperation) { + if ($table->getOption('joinedParents')) { + // just for bc! + $this->_insertCTIRecord($table, $record); + //-- + } else { + $this->processSingleInsert($record); + } + } + + $table->addRecord($record); + $table->getRecordListener()->postInsert($event); + $record->postInsert($event); + + return true; + } + + public function processSingleInsert(IPF_ORM_Record $record) + { + $fields = $record->getPrepared(); + $table = $record->getTable(); + + // Populate fields with a blank array so that a blank records can be inserted + if (empty($fields)) { + foreach ($table->getFieldNames() as $field) { + $fields[$field] = null; + } + } + + $identifier = (array) $table->getIdentifier(); + + $seq = $record->getTable()->sequenceName; + + if ( ! empty($seq)) { + $id = $this->conn->sequence->nextId($seq); + $seqName = $table->getIdentifier(); + $fields[$seqName] = $id; + + $record->assignIdentifier($id); + } + + $this->conn->insert($table, $fields); + + if (empty($seq) && count($identifier) == 1 && $identifier[0] == $table->getIdentifier() && + $table->getIdentifierType() != IPF_ORM::IDENTIFIER_NATURAL) { + if (strtolower($this->conn->getDriverName()) == 'pgsql') { + $seq = $table->getTableName() . '_' . $identifier[0]; + } + + $id = $this->conn->sequence->lastInsertId($seq); + + if ( ! $id) { + throw new IPF_ORM_Exception("Couldn't get last insert identifier."); + } + $record->assignIdentifier($id); + } else { + $record->assignIdentifier(true); + } + } + + public function buildFlushTree(array $tables) + { + // determine classes to order. only necessary because the $tables param + // can contain strings or table objects... + $classesToOrder = array(); + foreach ($tables as $table) { + if ( ! ($table instanceof IPF_ORM_Table)) { + $table = $this->conn->getTable($table, false); + } + $classesToOrder[] = $table->getComponentName(); + } + $classesToOrder = array_unique($classesToOrder); + + if (count($classesToOrder) < 2) { + return $classesToOrder; + } + + // build the correct order + $flushList = array(); + foreach ($classesToOrder as $class) { + $table = $this->conn->getTable($class, false); + $currentClass = $table->getComponentName(); + + $index = array_search($currentClass, $flushList); + + if ($index === false) { + //echo "adding $currentClass to flushlist"; + $flushList[] = $currentClass; + $index = max(array_keys($flushList)); + } + + $rels = $table->getRelations(); + + // move all foreignkey relations to the beginning + foreach ($rels as $key => $rel) { + if ($rel instanceof IPF_ORM_Relation_ForeignKey) { + unset($rels[$key]); + array_unshift($rels, $rel); + } + } + + foreach ($rels as $rel) { + $relatedClassName = $rel->getTable()->getComponentName(); + + if ( ! in_array($relatedClassName, $classesToOrder)) { + continue; + } + + $relatedCompIndex = array_search($relatedClassName, $flushList); + $type = $rel->getType(); + + // skip self-referenced relations + if ($relatedClassName === $currentClass) { + continue; + } + + if ($rel instanceof IPF_ORM_Relation_ForeignKey) { + // the related component needs to come after this component in + // the list (since it holds the fk) + + if ($relatedCompIndex !== false) { + // the component is already in the list + if ($relatedCompIndex >= $index) { + // it's already in the right place + continue; + } + + unset($flushList[$index]); + // the related comp has the fk. so put "this" comp immediately + // before it in the list + array_splice($flushList, $relatedCompIndex, 0, $currentClass); + $index = $relatedCompIndex; + } else { + $flushList[] = $relatedClassName; + } + + } else if ($rel instanceof IPF_ORM_Relation_LocalKey) { + // the related component needs to come before the current component + // in the list (since this component holds the fk). + + if ($relatedCompIndex !== false) { + // already in flush list + if ($relatedCompIndex <= $index) { + // it's in the right place + continue; + } + + unset($flushList[$relatedCompIndex]); + // "this" comp has the fk. so put the related comp before it + // in the list + array_splice($flushList, $index, 0, $relatedClassName); + } else { + array_unshift($flushList, $relatedClassName); + $index++; + } + } else if ($rel instanceof IPF_ORM_Relation_Association) { + // the association class needs to come after both classes + // that are connected through it in the list (since it holds + // both fks) + + $assocTable = $rel->getAssociationFactory(); + $assocClassName = $assocTable->getComponentName(); + + if ($relatedCompIndex !== false) { + unset($flushList[$relatedCompIndex]); + } + + array_splice($flushList, $index, 0, $relatedClassName); + $index++; + + $index3 = array_search($assocClassName, $flushList); + + if ($index3 !== false) { + if ($index3 >= $index) { + continue; + } + + unset($flushList[$index]); + array_splice($flushList, $index3, 0, $assocClassName); + $index = $relatedCompIndex; + } else { + $flushList[] = $assocClassName; + } + } + } + } + + return array_values($flushList); + } + + private function _deleteCTIParents(IPF_ORM_Table $table, $record) + { + if ($table->getOption('joinedParents')) { + foreach (array_reverse($table->getOption('joinedParents')) as $parent) { + $parentTable = $table->getConnection()->getTable($parent); + $this->conn->delete($parentTable, $record->identifier()); + } + } + } + + private function _insertCTIRecord(IPF_ORM_Table $table, IPF_ORM_Record $record) + { + $dataSet = $this->_formatDataSet($record); + $component = $table->getComponentName(); + + $classes = $table->getOption('joinedParents'); + $classes[] = $component; + + foreach ($classes as $k => $parent) { + if ($k === 0) { + $rootRecord = new $parent(); + $rootRecord->merge($dataSet[$parent]); + $this->processSingleInsert($rootRecord); + $record->assignIdentifier($rootRecord->identifier()); + } else { + foreach ((array) $rootRecord->identifier() as $id => $value) { + $dataSet[$parent][$id] = $value; + } + + $this->conn->insert($this->conn->getTable($parent), $dataSet[$parent]); + } + } + } + + private function _updateCTIRecord(IPF_ORM_Table $table, IPF_ORM_Record $record) + { + $identifier = $record->identifier(); + $dataSet = $this->_formatDataSet($record); + + $component = $table->getComponentName(); + + $classes = $table->getOption('joinedParents'); + $classes[] = $component; + + foreach ($record as $field => $value) { + if ($value instanceof IPF_ORM_Record) { + if ( ! $value->exists()) { + $value->save(); + } + $record->set($field, $value->getIncremented()); + } + } + + foreach ($classes as $class) { + $parentTable = $this->conn->getTable($class); + + if ( ! array_key_exists($class, $dataSet)) { + continue; + } + + $this->conn->update($this->conn->getTable($class), $dataSet[$class], $identifier); + } + } + + private function _formatDataSet(IPF_ORM_Record $record) + { + $table = $record->getTable(); + $dataSet = array(); + $component = $table->getComponentName(); + $array = $record->getPrepared(); + + foreach ($table->getColumns() as $columnName => $definition) { + if ( ! isset($dataSet[$component])) { + $dataSet[$component] = array(); + } + + $fieldName = $table->getFieldName($columnName); + if (isset($definition['primary']) && $definition['primary']) { + continue; + } + + if ( ! array_key_exists($fieldName, $array)) { + continue; + } + + if (isset($definition['owner'])) { + $dataSet[$definition['owner']][$fieldName] = $array[$fieldName]; + } else { + $dataSet[$component][$fieldName] = $array[$fieldName]; + } + } + + return $dataSet; + } +} \ No newline at end of file diff --git a/ipf/orm/datadict.php b/ipf/orm/datadict.php new file mode 100644 index 0000000..77bcdc6 --- /dev/null +++ b/ipf/orm/datadict.php @@ -0,0 +1,50 @@ +{"_compare{$type}Definition"}($current, $previous); + + if ($previous['type'] != $type) { + $change['type'] = true; + } + + $previous_notnull = !empty($previous['notnull']) ? $previous['notnull'] : false; + $notnull = !empty($current['notnull']) ? $current['notnull'] : false; + if ($previous_notnull != $notnull) { + $change['notnull'] = true; + } + + $previous_default = array_key_exists('default', $previous) ? $previous['default'] : + ($previous_notnull ? '' : null); + $default = array_key_exists('default', $current) ? $current['default'] : + ($notnull ? '' : null); + if ($previous_default !== $default) { + $change['default'] = true; + } + + return $change; + } + + public function parseBoolean($value) + { + // parse booleans + if ($value == 'true') { + $value = 1; + } elseif ($value == 'false') { + $value = 0; + } + return $value; + } +} \ No newline at end of file diff --git a/ipf/orm/datadict/mysql.php b/ipf/orm/datadict/mysql.php new file mode 100644 index 0000000..de33b1c --- /dev/null +++ b/ipf/orm/datadict/mysql.php @@ -0,0 +1,373 @@ +conn->varchar_max_length; + } else { + $field['length'] = false; + } + } + + $length = ($field['length'] <= $this->conn->varchar_max_length) ? $field['length'] : false; + $fixed = (isset($field['fixed'])) ? $field['fixed'] : false; + + return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)') + : ($length ? 'VARCHAR(' . $length . ')' : 'TEXT'); + case 'clob': + if ( ! empty($field['length'])) { + $length = $field['length']; + if ($length <= 255) { + return 'TINYTEXT'; + } elseif ($length <= 65532) { + return 'TEXT'; + } elseif ($length <= 16777215) { + return 'MEDIUMTEXT'; + } + } + return 'LONGTEXT'; + case 'blob': + if ( ! empty($field['length'])) { + $length = $field['length']; + if ($length <= 255) { + return 'TINYBLOB'; + } elseif ($length <= 65532) { + return 'BLOB'; + } elseif ($length <= 16777215) { + return 'MEDIUMBLOB'; + } + } + return 'LONGBLOB'; + case 'enum': + if ($this->conn->getAttribute(IPF_ORM::ATTR_USE_NATIVE_ENUM)) { + $values = array(); + foreach ($field['values'] as $value) { + $values[] = $this->conn->quote($value, 'varchar'); + } + return 'ENUM('.implode(', ', $values).')'; + } + // fall back to integer + case 'integer': + case 'int': + if ( ! empty($field['length'])) { + $length = $field['length']; + if ($length <= 1) { + return 'TINYINT'; + } elseif ($length == 2) { + return 'SMALLINT'; + } elseif ($length == 3) { + return 'MEDIUMINT'; + } elseif ($length == 4) { + return 'INT'; + } elseif ($length > 4) { + return 'BIGINT'; + } + } + return 'INT'; + case 'boolean': + return 'TINYINT(1)'; + case 'date': + return 'DATE'; + case 'time': + return 'TIME'; + case 'timestamp': + return 'DATETIME'; + case 'float': + case 'double': + return 'DOUBLE'; + case 'decimal': + $length = !empty($field['length']) ? $field['length'] : 18; + $scale = !empty($field['scale']) ? $field['scale'] : $this->conn->getAttribute(IPF_ORM::ATTR_DECIMAL_PLACES); + return 'DECIMAL('.$length.','.$scale.')'; + case 'bit': + return 'BIT'; + } + throw new IPF_ORM_Exception('Unknown field type \'' . $field['type'] . '\'.'); + } + + public function getPortableDeclaration(array $field) + { + $dbType = strtolower($field['type']); + $dbType = strtok($dbType, '(), '); + if ($dbType == 'national') { + $dbType = strtok('(), '); + } + if (isset($field['length'])) { + $length = $field['length']; + $decimal = ''; + } else { + $length = strtok('(), '); + $decimal = strtok('(), '); + } + $type = array(); + $unsigned = $fixed = null; + + if ( ! isset($field['name'])) { + $field['name'] = ''; + } + + $values = null; + + switch ($dbType) { + case 'tinyint': + $type[] = 'integer'; + $type[] = 'boolean'; + if (preg_match('/^(is|has)/', $field['name'])) { + $type = array_reverse($type); + } + $unsigned = preg_match('/ unsigned/i', $field['type']); + $length = 1; + break; + case 'smallint': + $type[] = 'integer'; + $unsigned = preg_match('/ unsigned/i', $field['type']); + $length = 2; + break; + case 'mediumint': + $type[] = 'integer'; + $unsigned = preg_match('/ unsigned/i', $field['type']); + $length = 3; + break; + case 'int': + case 'integer': + $type[] = 'integer'; + $unsigned = preg_match('/ unsigned/i', $field['type']); + $length = 4; + break; + case 'bigint': + $type[] = 'integer'; + $unsigned = preg_match('/ unsigned/i', $field['type']); + $length = 8; + break; + case 'tinytext': + case 'mediumtext': + case 'longtext': + case 'text': + case 'text': + case 'varchar': + $fixed = false; + case 'string': + case 'char': + $type[] = 'string'; + if ($length == '1') { + $type[] = 'boolean'; + if (preg_match('/^(is|has)/', $field['name'])) { + $type = array_reverse($type); + } + } elseif (strstr($dbType, 'text')) { + $type[] = 'clob'; + if ($decimal == 'binary') { + $type[] = 'blob'; + } + } + if ($fixed !== false) { + $fixed = true; + } + break; + case 'enum': + $type[] = 'enum'; + preg_match_all('/\'((?:\'\'|[^\'])*)\'/', $field['type'], $matches); + $length = 0; + $fixed = false; + if (is_array($matches)) { + foreach ($matches[1] as &$value) { + $value = str_replace('\'\'', '\'', $value); + $length = max($length, strlen($value)); + } + if ($length == '1' && count($matches[1]) == 2) { + $type[] = 'boolean'; + if (preg_match('/^(is|has)/', $field['name'])) { + $type = array_reverse($type); + } + } + + $values = $matches[1]; + } + $type[] = 'integer'; + break; + case 'set': + $fixed = false; + $type[] = 'text'; + $type[] = 'integer'; + break; + case 'date': + $type[] = 'date'; + $length = null; + break; + case 'datetime': + case 'timestamp': + $type[] = 'timestamp'; + $length = null; + break; + case 'time': + $type[] = 'time'; + $length = null; + break; + case 'float': + case 'double': + case 'real': + $type[] = 'float'; + $unsigned = preg_match('/ unsigned/i', $field['type']); + break; + case 'unknown': + case 'decimal': + case 'numeric': + $type[] = 'decimal'; + $unsigned = preg_match('/ unsigned/i', $field['type']); + break; + case 'tinyblob': + case 'mediumblob': + case 'longblob': + case 'blob': + $type[] = 'blob'; + $length = null; + break; + case 'year': + $type[] = 'integer'; + $type[] = 'date'; + $length = null; + break; + case 'bit': + $type[] = 'bit'; + break; + default: + throw new IPF_ORM_Exception('unknown database attribute type: ' . $dbType); + } + + $length = ((int) $length == 0) ? null : (int) $length; + + if ($values === null) { + return array('type' => $type, 'length' => $length, 'unsigned' => $unsigned, 'fixed' => $fixed); + } else { + return array('type' => $type, 'length' => $length, 'unsigned' => $unsigned, 'fixed' => $fixed, 'values' => $values); + } + } + + public function getCharsetFieldDeclaration($charset) + { + return 'CHARACTER SET ' . $charset; + } + + public function getCollationFieldDeclaration($collation) + { + return 'COLLATE ' . $collation; + } + + public function getIntegerDeclaration($name, $field) + { + $default = $autoinc = ''; + if ( ! empty($field['autoincrement'])) { + $autoinc = ' AUTO_INCREMENT'; + } elseif (array_key_exists('default', $field)) { + if ($field['default'] === '') { + $field['default'] = empty($field['notnull']) ? null : 0; + } + if (is_null($field['default'])) { + $default = ' DEFAULT NULL'; + } else { + $default = ' DEFAULT '.$this->conn->quote($field['default']); + } + } + + $notnull = (isset($field['notnull']) && $field['notnull']) ? ' NOT NULL' : ''; + $unsigned = (isset($field['unsigned']) && $field['unsigned']) ? ' UNSIGNED' : ''; + + $name = $this->conn->quoteIdentifier($name, true); + + return $name . ' ' . $this->getNativeDeclaration($field) . $unsigned . $default . $notnull . $autoinc; + } +} \ No newline at end of file diff --git a/ipf/orm/event.php b/ipf/orm/event.php new file mode 100644 index 0000000..c52fd29 --- /dev/null +++ b/ipf/orm/event.php @@ -0,0 +1,194 @@ +_invoker = $invoker; + $this->_code = $code; + $this->_query = $query; + $this->_params = $params; + } + + public function getQuery() + { + return $this->_query; + } + + public function getName() + { + switch ($this->_code) { + case self::CONN_QUERY: + return 'query'; + case self::CONN_EXEC: + return 'exec'; + case self::CONN_PREPARE: + return 'prepare'; + case self::CONN_CONNECT: + return 'connect'; + case self::CONN_CLOSE: + return 'close'; + case self::CONN_ERROR: + return 'error'; + + case self::STMT_EXECUTE: + return 'execute'; + case self::STMT_FETCH: + return 'fetch'; + case self::STMT_FETCHALL: + return 'fetch all'; + + case self::TX_BEGIN: + return 'begin'; + case self::TX_COMMIT: + return 'commit'; + case self::TX_ROLLBACK: + return 'rollback'; + + case self::SAVEPOINT_CREATE: + return 'create savepoint'; + case self::SAVEPOINT_ROLLBACK: + return 'rollback savepoint'; + case self::SAVEPOINT_COMMIT: + return 'commit savepoint'; + + case self::RECORD_DELETE: + return 'delete record'; + case self::RECORD_SAVE: + return 'save record'; + case self::RECORD_UPDATE: + return 'update record'; + case self::RECORD_INSERT: + return 'insert record'; + case self::RECORD_SERIALIZE: + return 'serialize record'; + case self::RECORD_UNSERIALIZE: + return 'unserialize record'; + case self::RECORD_DQL_SELECT: + return 'select records'; + case self::RECORD_DQL_DELETE: + return 'delete records'; + case self::RECORD_DQL_UPDATE: + return 'update records'; + } + } + + public function getCode() + { + return $this->_code; + } + + public function __get($option) + { + if ( ! isset($this->_options[$option])) { + return null; + } + + return $this->_options[$option]; + } + + public function skipOperation() + { + $this->_options['skipOperation'] = true; + + return $this; + } + + public function __set($option, $value) + { + $this->_options[$option] = $value; + + return $this; + } + + public function set($option, &$value) + { + $this->_options[$option] =& $value; + + return $this; + } + + public function start() + { + $this->_startedMicrotime = microtime(true); + } + + public function hasEnded() + { + return ($this->_endedMicrotime != null); + } + + public function end() + { + $this->_endedMicrotime = microtime(true); + + return $this; + } + + public function getInvoker() + { + return $this->_invoker; + } + + public function setInvoker($invoker) + { + $this->_invoker = $invoker; + } + + public function getParams() + { + return $this->_params; + } + + public function getElapsedSecs() + { + if (is_null($this->_endedMicrotime)) { + return false; + } + return ($this->_endedMicrotime - $this->_startedMicrotime); + } +} diff --git a/ipf/orm/eventlistener.php b/ipf/orm/eventlistener.php new file mode 100644 index 0000000..1aea865 --- /dev/null +++ b/ipf/orm/eventlistener.php @@ -0,0 +1,38 @@ + 'unknown error', + IPF_ORM::ERR_ALREADY_EXISTS => 'already exists', + IPF_ORM::ERR_CANNOT_CREATE => 'can not create', + IPF_ORM::ERR_CANNOT_ALTER => 'can not alter', + IPF_ORM::ERR_CANNOT_REPLACE => 'can not replace', + IPF_ORM::ERR_CANNOT_DELETE => 'can not delete', + IPF_ORM::ERR_CANNOT_DROP => 'can not drop', + IPF_ORM::ERR_CONSTRAINT => 'constraint violation', + IPF_ORM::ERR_CONSTRAINT_NOT_NULL=> 'null value violates not-null constraint', + IPF_ORM::ERR_DIVZERO => 'division by zero', + IPF_ORM::ERR_INVALID => 'invalid', + IPF_ORM::ERR_INVALID_DATE => 'invalid date or time', + IPF_ORM::ERR_INVALID_NUMBER => 'invalid number', + IPF_ORM::ERR_MISMATCH => 'mismatch', + IPF_ORM::ERR_NODBSELECTED => 'no database selected', + IPF_ORM::ERR_NOSUCHFIELD => 'no such field', + IPF_ORM::ERR_NOSUCHTABLE => 'no such table', + IPF_ORM::ERR_NOT_CAPABLE => 'IPF backend not capable', + IPF_ORM::ERR_NOT_FOUND => 'not found', + IPF_ORM::ERR_NOT_LOCKED => 'not locked', + IPF_ORM::ERR_SYNTAX => 'syntax error', + IPF_ORM::ERR_UNSUPPORTED => 'not supported', + IPF_ORM::ERR_VALUE_COUNT_ON_ROW => 'value count on row', + IPF_ORM::ERR_INVALID_DSN => 'invalid DSN', + IPF_ORM::ERR_CONNECT_FAILED => 'connect failed', + IPF_ORM::ERR_NEED_MORE_DATA => 'insufficient data supplied', + IPF_ORM::ERR_EXTENSION_NOT_FOUND=> 'extension not found', + IPF_ORM::ERR_NOSUCHDB => 'no such database', + IPF_ORM::ERR_ACCESS_VIOLATION => 'insufficient permissions', + IPF_ORM::ERR_LOADMODULE => 'error while including on demand module', + IPF_ORM::ERR_TRUNCATED => 'truncated', + IPF_ORM::ERR_DEADLOCK => 'deadlock detected', + ); + + public function errorMessage($value = null) + { + if (is_null($value)) { + return self::$_errorMessages; + } + + return isset(self::$_errorMessages[$value]) ? + self::$_errorMessages[$value] : self::$_errorMessages[IPF_ORM::ERR]; + } + +} + + + + diff --git a/ipf/orm/exception/adapter.php b/ipf/orm/exception/adapter.php new file mode 100644 index 0000000..7f8ce00 --- /dev/null +++ b/ipf/orm/exception/adapter.php @@ -0,0 +1,3 @@ + 'unknown error', + IPF_ORM::ERR_ALREADY_EXISTS => 'already exists', + IPF_ORM::ERR_CANNOT_CREATE => 'can not create', + IPF_ORM::ERR_CANNOT_ALTER => 'can not alter', + IPF_ORM::ERR_CANNOT_REPLACE => 'can not replace', + IPF_ORM::ERR_CANNOT_DELETE => 'can not delete', + IPF_ORM::ERR_CANNOT_DROP => 'can not drop', + IPF_ORM::ERR_CONSTRAINT => 'constraint violation', + IPF_ORM::ERR_CONSTRAINT_NOT_NULL=> 'null value violates not-null constraint', + IPF_ORM::ERR_DIVZERO => 'division by zero', + IPF_ORM::ERR_INVALID => 'invalid', + IPF_ORM::ERR_INVALID_DATE => 'invalid date or time', + IPF_ORM::ERR_INVALID_NUMBER => 'invalid number', + IPF_ORM::ERR_MISMATCH => 'mismatch', + IPF_ORM::ERR_NODBSELECTED => 'no database selected', + IPF_ORM::ERR_NOSUCHFIELD => 'no such field', + IPF_ORM::ERR_NOSUCHTABLE => 'no such table', + IPF_ORM::ERR_NOT_CAPABLE => 'IPF_ORM backend not capable', + IPF_ORM::ERR_NOT_FOUND => 'not found', + IPF_ORM::ERR_NOT_LOCKED => 'not locked', + IPF_ORM::ERR_SYNTAX => 'syntax error', + IPF_ORM::ERR_UNSUPPORTED => 'not supported', + IPF_ORM::ERR_VALUE_COUNT_ON_ROW => 'value count on row', + IPF_ORM::ERR_INVALID_DSN => 'invalid DSN', + IPF_ORM::ERR_CONNECT_FAILED => 'connect failed', + IPF_ORM::ERR_NEED_MORE_DATA => 'insufficient data supplied', + IPF_ORM::ERR_EXTENSION_NOT_FOUND=> 'extension not found', + IPF_ORM::ERR_NOSUCHDB => 'no such database', + IPF_ORM::ERR_ACCESS_VIOLATION => 'insufficient permissions', + IPF_ORM::ERR_LOADMODULE => 'error while including on demand module', + IPF_ORM::ERR_TRUNCATED => 'truncated', + IPF_ORM::ERR_DEADLOCK => 'deadlock detected', + ); + + protected $portableCode; + + public function getPortableCode() + { + return $this->portableCode; + } + + public function getPortableMessage() + { + return self::errorMessage($this->portableCode); + } + + public function errorMessage($value = null) + { + return isset(self::$errorMessages[$value]) ? + self::$errorMessages[$value] : self::$errorMessages[IPF_ORM::ERR]; + } + + public function processErrorInfo(array $errorInfo) + { } +} \ No newline at end of file diff --git a/ipf/orm/exception/locator.php b/ipf/orm/exception/locator.php new file mode 100644 index 0000000..d39308f --- /dev/null +++ b/ipf/orm/exception/locator.php @@ -0,0 +1,3 @@ + IPF_ORM::ERR_CANNOT_CREATE, + 1005 => IPF_ORM::ERR_CANNOT_CREATE, + 1006 => IPF_ORM::ERR_CANNOT_CREATE, + 1007 => IPF_ORM::ERR_ALREADY_EXISTS, + 1008 => IPF_ORM::ERR_CANNOT_DROP, + 1022 => IPF_ORM::ERR_ALREADY_EXISTS, + 1044 => IPF_ORM::ERR_ACCESS_VIOLATION, + 1046 => IPF_ORM::ERR_NODBSELECTED, + 1048 => IPF_ORM::ERR_CONSTRAINT, + 1049 => IPF_ORM::ERR_NOSUCHDB, + 1050 => IPF_ORM::ERR_ALREADY_EXISTS, + 1051 => IPF_ORM::ERR_NOSUCHTABLE, + 1054 => IPF_ORM::ERR_NOSUCHFIELD, + 1061 => IPF_ORM::ERR_ALREADY_EXISTS, + 1062 => IPF_ORM::ERR_ALREADY_EXISTS, + 1064 => IPF_ORM::ERR_SYNTAX, + 1091 => IPF_ORM::ERR_NOT_FOUND, + 1100 => IPF_ORM::ERR_NOT_LOCKED, + 1136 => IPF_ORM::ERR_VALUE_COUNT_ON_ROW, + 1142 => IPF_ORM::ERR_ACCESS_VIOLATION, + 1146 => IPF_ORM::ERR_NOSUCHTABLE, + 1216 => IPF_ORM::ERR_CONSTRAINT, + 1217 => IPF_ORM::ERR_CONSTRAINT, + 1451 => IPF_ORM::ERR_CONSTRAINT, + ); + public function processErrorInfo(array $errorInfo) + { + $code = $errorInfo[1]; + if (isset(self::$errorCodeMap[$code])) { + $this->portableCode = self::$errorCodeMap[$code]; + return true; + } + return false; + } +} diff --git a/ipf/orm/exception/orm.php b/ipf/orm/exception/orm.php new file mode 100644 index 0000000..12b42aa --- /dev/null +++ b/ipf/orm/exception/orm.php @@ -0,0 +1,54 @@ + 'unknown error', + IPF_ORM::ERR_ALREADY_EXISTS => 'already exists', + IPF_ORM::ERR_CANNOT_CREATE => 'can not create', + IPF_ORM::ERR_CANNOT_ALTER => 'can not alter', + IPF_ORM::ERR_CANNOT_REPLACE => 'can not replace', + IPF_ORM::ERR_CANNOT_DELETE => 'can not delete', + IPF_ORM::ERR_CANNOT_DROP => 'can not drop', + IPF_ORM::ERR_CONSTRAINT => 'constraint violation', + IPF_ORM::ERR_CONSTRAINT_NOT_NULL=> 'null value violates not-null constraint', + IPF_ORM::ERR_DIVZERO => 'division by zero', + IPF_ORM::ERR_INVALID => 'invalid', + IPF_ORM::ERR_INVALID_DATE => 'invalid date or time', + IPF_ORM::ERR_INVALID_NUMBER => 'invalid number', + IPF_ORM::ERR_MISMATCH => 'mismatch', + IPF_ORM::ERR_NODBSELECTED => 'no database selected', + IPF_ORM::ERR_NOSUCHFIELD => 'no such field', + IPF_ORM::ERR_NOSUCHTABLE => 'no such table', + IPF_ORM::ERR_NOT_CAPABLE => 'IPF backend not capable', + IPF_ORM::ERR_NOT_FOUND => 'not found', + IPF_ORM::ERR_NOT_LOCKED => 'not locked', + IPF_ORM::ERR_SYNTAX => 'syntax error', + IPF_ORM::ERR_UNSUPPORTED => 'not supported', + IPF_ORM::ERR_VALUE_COUNT_ON_ROW => 'value count on row', + IPF_ORM::ERR_INVALID_DSN => 'invalid DSN', + IPF_ORM::ERR_CONNECT_FAILED => 'connect failed', + IPF_ORM::ERR_NEED_MORE_DATA => 'insufficient data supplied', + IPF_ORM::ERR_EXTENSION_NOT_FOUND=> 'extension not found', + IPF_ORM::ERR_NOSUCHDB => 'no such database', + IPF_ORM::ERR_ACCESS_VIOLATION => 'insufficient permissions', + IPF_ORM::ERR_LOADMODULE => 'error while including on demand module', + IPF_ORM::ERR_TRUNCATED => 'truncated', + IPF_ORM::ERR_DEADLOCK => 'deadlock detected', + ); + + public function errorMessage($value = null) + { + if (is_null($value)) { + return self::$_errorMessages; + } + + return isset(self::$_errorMessages[$value]) ? + self::$_errorMessages[$value] : self::$_errorMessages[IPF_ORM::ERR]; + } + +} + + + + diff --git a/ipf/orm/exception/validator.php b/ipf/orm/exception/validator.php new file mode 100644 index 0000000..9d1b5de --- /dev/null +++ b/ipf/orm/exception/validator.php @@ -0,0 +1,49 @@ +invalid = $invalid; + parent::__construct($this->generateMessage()); + } + + public function getInvalidRecords() + { + return $this->invalid; + } + + public function getIterator() + { + return new ArrayIterator($this->invalid); + } + + public function count() + { + return count($this->invalid); + } + + public function __toString() + { + + return parent::__toString(); + } + + private function generateMessage() + { + $message = ""; + foreach ($this->invalid as $record) { + $message .= "Validaton error in class " . get_class($record) . " "; + } + return $message; + } + + public function inspect($function) + { + foreach ($this->invalid as $record) { + call_user_func($function, $record->getErrorStack()); + } + } +} \ No newline at end of file diff --git a/ipf/orm/export.php b/ipf/orm/export.php new file mode 100644 index 0000000..7f5911b --- /dev/null +++ b/ipf/orm/export.php @@ -0,0 +1,658 @@ + '', + 'boolean' => true, + 'integer' => 0, + 'decimal' => 0.0, + 'float' => 0.0, + 'timestamp' => '1970-01-01 00:00:00', + 'time' => '00:00:00', + 'date' => '1970-01-01', + 'clob' => '', + 'blob' => '', + 'string' => '' + ); + + public function dropDatabase($database) + { + $this->conn->execute($this->dropDatabaseSql($database)); + } + + public function dropDatabaseSql($database) + { + throw new IPF_ORM_Exception('Drop database not supported by this driver.'); + } + + public function dropTableSql($table) + { + return 'DROP TABLE ' . $this->conn->quoteIdentifier($table); + } + + public function dropTable($table) + { + $this->conn->execute($this->dropTableSql($table)); + } + + public function dropIndex($table, $name) + { + return $this->conn->exec($this->dropIndexSql($table, $name)); + } + + public function dropIndexSql($table, $name) + { + $name = $this->conn->quoteIdentifier($this->conn->formatter->getIndexName($name)); + + return 'DROP INDEX ' . $name; + } + + public function dropConstraint($table, $name, $primary = false) + { + $table = $this->conn->quoteIdentifier($table); + $name = $this->conn->quoteIdentifier($name); + + return $this->conn->exec('ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $name); + } + + public function dropForeignKey($table, $name) + { + return $this->dropConstraint($table, $name); + } + + public function dropSequence($sequenceName) + { + $this->conn->exec($this->dropSequenceSql($sequenceName)); + } + + public function dropSequenceSql($sequenceName) + { + throw new IPF_ORM_Exception('Drop sequence not supported by this driver.'); + } + + public function createDatabase($database) + { + $this->conn->execute($this->createDatabaseSql($database)); + } + + public function createDatabaseSql($database) + { + throw new IPF_ORM_Exception('Create database not supported by this driver.'); + } + + public function createTableSql($name, array $fields, array $options = array()) + { + if ( ! $name) { + throw new IPF_ORM_Exception('no valid table name specified'); + } + + if (empty($fields)) { + throw new IPF_ORM_Exception('no fields specified for table ' . $name); + } + + $queryFields = $this->getFieldDeclarationList($fields); + + + if (isset($options['primary']) && ! empty($options['primary'])) { + $queryFields .= ', PRIMARY KEY(' . implode(', ', array_values($options['primary'])) . ')'; + } + + if (isset($options['indexes']) && ! empty($options['indexes'])) { + foreach($options['indexes'] as $index => $definition) { + $queryFields .= ', ' . $this->getIndexDeclaration($index, $definition); + } + } + + $query = 'CREATE TABLE ' . $this->conn->quoteIdentifier($name, true) . ' (' . $queryFields; + + $check = $this->getCheckDeclaration($fields); + + if ( ! empty($check)) { + $query .= ', ' . $check; + } + + $query .= ')'; + + $sql[] = $query; + + if (isset($options['foreignKeys'])) { + + foreach ((array) $options['foreignKeys'] as $k => $definition) { + if (is_array($definition)) { + $sql[] = $this->createForeignKeySql($name, $definition); + } + } + } + return $sql; + } + + public function createTable($name, array $fields, array $options = array()) + { + $sql = (array) $this->createTableSql($name, $fields, $options); + + foreach ($sql as $query) { + $this->conn->execute($query); + } + } + + public function createSequence($seqName, $start = 1, array $options = array()) + { + return $this->conn->execute($this->createSequenceSql($seqName, $start = 1, $options)); + } + + public function createSequenceSql($seqName, $start = 1, array $options = array()) + { + throw new IPF_ORM_Exception('Create sequence not supported by this driver.'); + } + + public function createConstraint($table, $name, $definition) + { + $sql = $this->createConstraintSql($table, $name, $definition); + + return $this->conn->exec($sql); + } + + public function createConstraintSql($table, $name, $definition) + { + $table = $this->conn->quoteIdentifier($table); + $name = $this->conn->quoteIdentifier($this->conn->formatter->getIndexName($name)); + $query = 'ALTER TABLE ' . $table . ' ADD CONSTRAINT ' . $name; + + if (isset($definition['primary']) && $definition['primary']) { + $query .= ' PRIMARY KEY'; + } elseif (isset($definition['unique']) && $definition['unique']) { + $query .= ' UNIQUE'; + } + + $fields = array(); + foreach (array_keys($definition['fields']) as $field) { + $fields[] = $this->conn->quoteIdentifier($field, true); + } + $query .= ' ('. implode(', ', $fields) . ')'; + + return $query; + } + + public function createIndex($table, $name, array $definition) + { + return $this->conn->execute($this->createIndexSql($table, $name, $definition)); + } + + public function createIndexSql($table, $name, array $definition) + { + $table = $this->conn->quoteIdentifier($table); + $name = $this->conn->quoteIdentifier($name); + $type = ''; + + if (isset($definition['type'])) { + switch (strtolower($definition['type'])) { + case 'unique': + $type = strtoupper($definition['type']) . ' '; + break; + default: + throw new IPF_ORM_Exception('Unknown index type ' . $definition['type']); + } + } + + $query = 'CREATE ' . $type . 'INDEX ' . $name . ' ON ' . $table; + + $fields = array(); + foreach ($definition['fields'] as $field) { + $fields[] = $this->conn->quoteIdentifier($field); + } + $query .= ' (' . implode(', ', $fields) . ')'; + + return $query; + } + + public function createForeignKeySql($table, array $definition) + { + $table = $this->conn->quoteIdentifier($table); + + $query = 'ALTER TABLE ' . $table . ' ADD ' . $this->getForeignKeyDeclaration($definition); + + return $query; + } + + public function createForeignKey($table, array $definition) + { + $sql = $this->createForeignKeySql($table, $definition); + + return $this->conn->execute($sql); + } + + public function alterTable($name, array $changes, $check = false) + { + $sql = $this->alterTableSql($name, $changes, $check); + + if (is_string($sql) && $sql) { + $this->conn->execute($sql); + } + } + + public function alterTableSql($name, array $changes, $check = false) + { + throw new IPF_ORM_Exception('Alter table not supported by this driver.'); + } + + public function getFieldDeclarationList(array $fields) + { + foreach ($fields as $fieldName => $field) { + $query = $this->getDeclaration($fieldName, $field); + + $queryFields[] = $query; + } + return implode(', ', $queryFields); + } + + public function getDeclaration($name, array $field) + { + + $default = $this->getDefaultFieldDeclaration($field); + + $charset = (isset($field['charset']) && $field['charset']) ? + ' ' . $this->getCharsetFieldDeclaration($field['charset']) : ''; + + $collation = (isset($field['collation']) && $field['collation']) ? + ' ' . $this->getCollationFieldDeclaration($field['collation']) : ''; + + $notnull = (isset($field['notnull']) && $field['notnull']) ? ' NOT NULL' : ''; + + $unique = (isset($field['unique']) && $field['unique']) ? + ' ' . $this->getUniqueFieldDeclaration() : ''; + + $check = (isset($field['check']) && $field['check']) ? + ' ' . $field['check'] : ''; + + $method = 'get' . $field['type'] . 'Declaration'; + + if (method_exists($this->conn->dataDict, $method)) { + return $this->conn->dataDict->$method($name, $field); + } else { + $dec = $this->conn->dataDict->getNativeDeclaration($field); + } + return $this->conn->quoteIdentifier($name, true) . ' ' . $dec . $charset . $default . $notnull . $unique . $check . $collation; + } + + public function getDefaultFieldDeclaration($field) + { + $default = ''; + if (isset($field['default'])) { + if ($field['default'] === '') { + $field['default'] = empty($field['notnull']) + ? null : $this->valid_default_values[$field['type']]; + + if ($field['default'] === '' && + ($this->conn->getAttribute(IPF_ORM::ATTR_PORTABILITY) & IPF_ORM::PORTABILITY_EMPTY_TO_NULL)) { + $field['default'] = null; + } + } + + if ($field['type'] === 'boolean') { + $field['default'] = $this->conn->convertBooleans($field['default']); + } + $default = ' DEFAULT ' . $this->conn->quote($field['default'], $field['type']); + } + return $default; + } + + public function getCheckDeclaration(array $definition) + { + $constraints = array(); + foreach ($definition as $field => $def) { + if (is_string($def)) { + $constraints[] = 'CHECK (' . $def . ')'; + } else { + if (isset($def['min'])) { + $constraints[] = 'CHECK (' . $field . ' >= ' . $def['min'] . ')'; + } + + if (isset($def['max'])) { + $constraints[] = 'CHECK (' . $field . ' <= ' . $def['max'] . ')'; + } + } + } + + return implode(', ', $constraints); + } + + public function getIndexDeclaration($name, array $definition) + { + $name = $this->conn->quoteIdentifier($name); + $type = ''; + + if (isset($definition['type'])) { + if (strtolower($definition['type']) == 'unique') { + $type = strtoupper($definition['type']) . ' '; + } else { + throw new IPF_ORM_Exception('Unknown index type ' . $definition['type']); + } + } + + if ( ! isset($definition['fields']) || ! is_array($definition['fields'])) { + throw new IPF_ORM_Exception('No index columns given.'); + } + + $query = $type . 'INDEX ' . $name; + + $query .= ' (' . $this->getIndexFieldDeclarationList($definition['fields']) . ')'; + + return $query; + } + + public function getIndexFieldDeclarationList(array $fields) + { + $ret = array(); + foreach ($fields as $field => $definition) { + if (is_array($definition)) { + $ret[] = $this->conn->quoteIdentifier($field); + } else { + $ret[] = $this->conn->quoteIdentifier($definition); + } + } + return implode(', ', $ret); + } + + public function getTemporaryTableQuery() + { + return 'TEMPORARY'; + } + + public function getForeignKeyDeclaration(array $definition) + { + $sql = $this->getForeignKeyBaseDeclaration($definition); + $sql .= $this->getAdvancedForeignKeyOptions($definition); + + return $sql; + } + + public function getAdvancedForeignKeyOptions(array $definition) + { + $query = ''; + if ( ! empty($definition['onUpdate'])) { + $query .= ' ON UPDATE ' . $this->getForeignKeyRefentialAction($definition['onUpdate']); + } + if ( ! empty($definition['onDelete'])) { + $query .= ' ON DELETE ' . $this->getForeignKeyRefentialAction($definition['onDelete']); + } + return $query; + } + + public function getForeignKeyReferentialAction($action) + { + $upper = strtoupper($action); + switch ($upper) { + case 'CASCADE': + case 'SET NULL': + case 'NO ACTION': + case 'RESTRICT': + case 'SET DEFAULT': + return $upper; + break; + default: + throw new IPF_ORM_Exception('Unknown foreign key referential action \'' . $upper . '\' given.'); + } + } + + public function getForeignKeyBaseDeclaration(array $definition) + { + $sql = ''; + if (isset($definition['name'])) { + $sql .= ' CONSTRAINT ' . $this->conn->quoteIdentifier($definition['name']) . ' '; + } + $sql .= 'FOREIGN KEY ('; + + if ( ! isset($definition['local'])) { + throw new IPF_ORM_Exception('Local reference field missing from definition.'); + } + if ( ! isset($definition['foreign'])) { + throw new IPF_ORM_Exception('Foreign reference field missing from definition.'); + } + if ( ! isset($definition['foreignTable'])) { + throw new IPF_ORM_Exception('Foreign reference table missing from definition.'); + } + + if ( ! is_array($definition['local'])) { + $definition['local'] = array($definition['local']); + } + if ( ! is_array($definition['foreign'])) { + $definition['foreign'] = array($definition['foreign']); + } + + $sql .= implode(', ', array_map(array($this->conn, 'quoteIdentifier'), $definition['local'])) + . ') REFERENCES ' + . $this->conn->quoteIdentifier($definition['foreignTable']) . '(' + . implode(', ', array_map(array($this->conn, 'quoteIdentifier'), $definition['foreign'])) . ')'; + + return $sql; + } + + public function getUniqueFieldDeclaration() + { + return 'UNIQUE'; + } + + public function getCharsetFieldDeclaration($charset) + { + return ''; + } + + public function getCollationFieldDeclaration($collation) + { + return ''; + } + + public function exportSchema($directory = null) + { + if ($directory !== null) { + $models = IPF_ORM::filterInvalidModels(IPF_ORM::loadModels($directory)); + } else { + $models = IPF_ORM::getLoadedModels(); + } + $this->exportClasses($models); + } + + public function exportSortedClassesSql($classes, $groupByConnection = true) + { + $connections = array(); + foreach ($classes as $class) { + $connection = IPF_ORM_Manager::getInstance()->getConnectionForComponent($class); + $connectionName = $connection->getName(); + + if ( ! isset($connections[$connectionName])) { + $connections[$connectionName] = array( + 'create_tables' => array(), + 'create_sequences' => array(), + 'create_indexes' => array(), + 'alters' => array() + ); + } + + $sql = $connection->export->exportClassesSql(array($class)); + + // Build array of all the creates + // We need these to happen first + foreach ($sql as $key => $query) { + // If create table statement + if (substr($query, 0, strlen('CREATE TABLE')) == 'CREATE TABLE') { + $connections[$connectionName]['create_tables'][] = $query; + + unset($sql[$key]); + continue; + } + + // If create sequence statement + if (substr($query, 0, strlen('CREATE SEQUENCE')) == 'CREATE SEQUENCE') { + $connections[$connectionName]['create_sequences'][] = $query; + + unset($sql[$key]); + continue; + } + + // If create index statement + if (preg_grep("/CREATE .* INDEX/", array($query))) { + $connections[$connectionName]['create_indexes'][] = $query; + + unset($sql[$key]); + continue; + } + + // If alter table statement + if (substr($query, 0, strlen('ALTER TABLE')) == 'ALTER TABLE') { + $connections[$connectionName]['alters'][] = $query; + + unset($sql[$key]); + continue; + } + } + } + + // Loop over all the sql again to merge everything together so it is in the correct order + $build = array(); + foreach ($connections as $connectionName => $sql) { + $build[$connectionName] = array_merge($sql['create_tables'], $sql['create_sequences'], $sql['create_indexes'], $sql['alters']); + } + + if ( ! $groupByConnection) { + $new = array(); + foreach($build as $connectionname => $sql) { + $new = array_merge($new, $sql); + } + $build = $new; + } + return $build; + } + + public function exportClasses(array $classes) + { + $queries = $this->exportSortedClassesSql($classes); + + foreach ($queries as $connectionName => $sql) { + $connection = IPF_ORM_Manager::getInstance()->getConnection($connectionName); + + $connection->beginTransaction(); + + foreach ($sql as $query) { + try { + $connection->exec($query); + } catch (IPF_ORM_Exception $e) { + // we only want to silence table already exists errors + if ($e->getPortableCode() !== IPF_ORM::ERR_ALREADY_EXISTS) { + $connection->rollback(); + throw new IPF_ORM_Exception($e->getMessage() . '. Failing Query: ' . $query); + } + } + } + $connection->commit(); + } + } + + public function exportClassesSql(array $classes) + { + $models = IPF_ORM::filterInvalidModels($classes); + + $sql = array(); + + foreach ($models as $name) { + $record = new $name(); + $table = $record->getTable(); + + $parents = $table->getOption('joinedParents'); + + foreach ($parents as $parent) { + $data = $table->getConnection()->getTable($parent)->getExportableFormat(); + + $query = $this->conn->export->createTableSql($data['tableName'], $data['columns'], $data['options']); + + $sql = array_merge($sql, (array) $query); + } + + $data = $table->getExportableFormat(); + + $query = $this->conn->export->createTableSql($data['tableName'], $data['columns'], $data['options']); + + if (is_array($query)) { + $sql = array_merge($sql, $query); + } else { + $sql[] = $query; + } + + if ($table->getAttribute(IPF_ORM::ATTR_EXPORT) & IPF_ORM::EXPORT_PLUGINS) { + $sql = array_merge($sql, $this->exportGeneratorsSql($table)); + } + } + + $sql = array_unique($sql); + + rsort($sql); + + return $sql; + } + + public function getAllGenerators(IPF_ORM_Table $table) + { + $generators = array(); + + foreach ($table->getGenerators() as $name => $generator) { + if ($generator === null) { + continue; + } + + $generators[] = $generator; + + $generatorTable = $generator->getTable(); + + if ($generatorTable instanceof IPF_ORM_Table) { + $generators = array_merge($generators, $this->getAllGenerators($generatorTable)); + } + } + + return $generators; + } + + public function exportGeneratorsSql(IPF_ORM_Table $table) + { + $sql = array(); + + foreach ($this->getAllGenerators($table) as $name => $generator) { + $table = $generator->getTable(); + + // Make sure plugin has a valid table + if ($table instanceof IPF_ORM_Table) { + $data = $table->getExportableFormat(); + + $query = $this->conn->export->createTableSql($data['tableName'], $data['columns'], $data['options']); + + $sql = array_merge($sql, (array) $query); + } + } + + return $sql; + } + + public function exportSql($directory = null) + { + if ($directory !== null) { + $models = IPF_ORM::filterInvalidModels(IPF_ORM::loadModels($directory)); + } else { + $models = IPF_ORM::getLoadedModels(); + } + + return $this->exportSortedClassesSql($models, false); + } + + public function exportTable(IPF_ORM_Table $table) + { + try { + $data = $table->getExportableFormat(); + + $this->conn->export->createTable($data['tableName'], $data['columns'], $data['options']); + } catch(IPF_ORM_Exception $e) { + // we only want to silence table already exists errors + if ($e->getPortableCode() !== IPF_ORM::ERR_ALREADY_EXISTS) { + throw $e; + } + } + } +} \ No newline at end of file diff --git a/ipf/orm/export/mysql.php b/ipf/orm/export/mysql.php new file mode 100644 index 0000000..a5cabf8 --- /dev/null +++ b/ipf/orm/export/mysql.php @@ -0,0 +1,447 @@ +conn->quoteIdentifier($name, true); + } + + public function dropDatabaseSql($name) + { + return 'DROP DATABASE ' . $this->conn->quoteIdentifier($name); + } + + public function createTableSql($name, array $fields, array $options = array()) + { + if ( ! $name) + throw new IPF_ORM_Exception('no valid table name specified'); + + if (empty($fields)) { + throw new IPF_ORM_Exception('no fields specified for table "'.$name.'"'); + } + $queryFields = $this->getFieldDeclarationList($fields); + + // build indexes for all foreign key fields (needed in MySQL!!) + if (isset($options['foreignKeys'])) { + foreach ($options['foreignKeys'] as $fk) { + $local = $fk['local']; + $found = false; + if (isset($options['indexes'])) { + foreach ($options['indexes'] as $definition) { + if (is_string($definition['fields'])) { + // Check if index already exists on the column + $found = ($local == $definition['fields']); + } else if (in_array($local, $definition['fields']) && count($definition['fields']) === 1) { + // Index already exists on the column + $found = true; + } + } + } + if (isset($options['primary']) && !empty($options['primary']) && + in_array($local, $options['primary'])) { + // field is part of the PK and therefore already indexed + $found = true; + } + + if ( ! $found) { + if (is_array($local)) { + foreach($local as $localidx) { + $options['indexes'][$localidx] = array('fields' => array($localidx => array())); + } + } else { + $options['indexes'][$local] = array('fields' => array($local => array())); + } + } + } + } + + // add all indexes + if (isset($options['indexes']) && ! empty($options['indexes'])) { + foreach($options['indexes'] as $index => $definition) { + $queryFields .= ', ' . $this->getIndexDeclaration($index, $definition); + } + } + + // attach all primary keys + if (isset($options['primary']) && ! empty($options['primary'])) { + $keyColumns = array_values($options['primary']); + $keyColumns = array_map(array($this->conn, 'quoteIdentifier'), $keyColumns); + $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; + } + + $query = 'CREATE TABLE ' . $this->conn->quoteIdentifier($name, true) . ' (' . $queryFields . ')'; + + $optionStrings = array(); + + if (isset($options['comment'])) { + $optionStrings['comment'] = 'COMMENT = ' . $this->conn->quote($options['comment'], 'text'); + } + if (isset($options['charset'])) { + $optionStrings['charset'] = 'DEFAULT CHARACTER SET ' . $options['charset']; + } + if (isset($options['collate'])) { + $optionStrings['collate'] = 'COLLATE ' . $options['collate']; + } + + $type = false; + + // get the type of the table + if (isset($options['type'])) { + $type = $options['type']; + } else { + $type = $this->conn->getAttribute(IPF_ORM::ATTR_DEFAULT_TABLE_TYPE); + } + + if ($type) { + $optionStrings[] = 'ENGINE = ' . $type; + } + + if ( ! empty($optionStrings)) { + $query.= ' '.implode(' ', $optionStrings); + } + $sql[] = $query; + + if (isset($options['foreignKeys'])) { + + foreach ((array) $options['foreignKeys'] as $k => $definition) { + if (is_array($definition)) { + $sql[] = $this->createForeignKeySql($name, $definition); + } + } + } + return $sql; + } + + public function getDeclaration($name, array $field) + { + + $default = $this->getDefaultFieldDeclaration($field); + + $charset = (isset($field['charset']) && $field['charset']) ? + ' ' . $this->getCharsetFieldDeclaration($field['charset']) : ''; + + $collation = (isset($field['collation']) && $field['collation']) ? + ' ' . $this->getCollationFieldDeclaration($field['collation']) : ''; + + $notnull = (isset($field['notnull']) && $field['notnull']) ? ' NOT NULL' : ''; + + $unique = (isset($field['unique']) && $field['unique']) ? + ' ' . $this->getUniqueFieldDeclaration() : ''; + + $check = (isset($field['check']) && $field['check']) ? + ' ' . $field['check'] : ''; + + $comment = (isset($field['comment']) && $field['comment']) ? + " COMMENT '" . $field['comment'] . "'" : ''; + + $method = 'get' . $field['type'] . 'Declaration'; + + if (method_exists($this->conn->dataDict, $method)) { + return $this->conn->dataDict->$method($name, $field); + } else { + $dec = $this->conn->dataDict->getNativeDeclaration($field); + } + return $this->conn->quoteIdentifier($name, true) . ' ' . $dec . $charset . $default . $notnull . $comment . $unique . $check . $collation; + } + + public function alterTableSql($name, array $changes, $check = false) + { + if ( ! $name) { + throw new IPF_ORM_Exception('no valid table name specified'); + } + foreach ($changes as $changeName => $change) { + switch ($changeName) { + case 'add': + case 'remove': + case 'change': + case 'rename': + case 'name': + break; + default: + throw new IPF_ORM_Exception('change type "' . $changeName . '" not yet supported'); + } + } + + if ($check) { + return true; + } + + $query = ''; + if ( ! empty($changes['name'])) { + $change_name = $this->conn->quoteIdentifier($changes['name']); + $query .= 'RENAME TO ' . $change_name; + } + + if ( ! empty($changes['add']) && is_array($changes['add'])) { + foreach ($changes['add'] as $fieldName => $field) { + if ($query) { + $query.= ', '; + } + $query.= 'ADD ' . $this->getDeclaration($fieldName, $field); + } + } + + if ( ! empty($changes['remove']) && is_array($changes['remove'])) { + foreach ($changes['remove'] as $fieldName => $field) { + if ($query) { + $query .= ', '; + } + $fieldName = $this->conn->quoteIdentifier($fieldName); + $query .= 'DROP ' . $fieldName; + } + } + + $rename = array(); + if ( ! empty($changes['rename']) && is_array($changes['rename'])) { + foreach ($changes['rename'] as $fieldName => $field) { + $rename[$field['name']] = $fieldName; + } + } + + if ( ! empty($changes['change']) && is_array($changes['change'])) { + foreach ($changes['change'] as $fieldName => $field) { + if ($query) { + $query.= ', '; + } + if (isset($rename[$fieldName])) { + $oldFieldName = $rename[$fieldName]; + unset($rename[$fieldName]); + } else { + $oldFieldName = $fieldName; + } + $oldFieldName = $this->conn->quoteIdentifier($oldFieldName, true); + $query .= 'CHANGE ' . $oldFieldName . ' ' + . $this->getDeclaration($fieldName, $field['definition']); + } + } + + if ( ! empty($rename) && is_array($rename)) { + foreach ($rename as $renameName => $renamedField) { + if ($query) { + $query.= ', '; + } + $field = $changes['rename'][$renamedField]; + $renamedField = $this->conn->quoteIdentifier($renamedField, true); + $query .= 'CHANGE ' . $renamedField . ' ' + . $this->getDeclaration($field['name'], $field['definition']); + } + } + + if ( ! $query) { + return false; + } + + $name = $this->conn->quoteIdentifier($name, true); + + return 'ALTER TABLE ' . $name . ' ' . $query; + } + + public function createSequence($sequenceName, $start = 1, array $options = array()) + { + $sequenceName = $this->conn->quoteIdentifier($sequenceName, true); + $seqcolName = $this->conn->quoteIdentifier($this->conn->getAttribute(IPF_ORM::ATTR_SEQCOL_NAME), true); + + $optionsStrings = array(); + + if (isset($options['comment']) && ! empty($options['comment'])) { + $optionsStrings['comment'] = 'COMMENT = ' . $this->conn->quote($options['comment'], 'string'); + } + + if (isset($options['charset']) && ! empty($options['charset'])) { + $optionsStrings['charset'] = 'DEFAULT CHARACTER SET ' . $options['charset']; + + if (isset($options['collate'])) { + $optionsStrings['charset'] .= ' COLLATE ' . $options['collate']; + } + } + + $type = false; + + if (isset($options['type'])) { + $type = $options['type']; + } else { + $type = $this->conn->getAttribute(IPF_ORM::ATTR_DEFAULT_TABLE_TYPE); + } + if ($type) { + $optionsStrings[] = 'ENGINE = ' . $type; + } + + + try { + $query = 'CREATE TABLE ' . $sequenceName + . ' (' . $seqcolName . ' BIGINT NOT NULL AUTO_INCREMENT, PRIMARY KEY (' + . $seqcolName . ')) ' . implode($optionsStrings, ' '); + + $res = $this->conn->exec($query); + } catch(IPF_ORM_Exception $e) { + throw new IPF_ORM_Exception('could not create sequence table'); + } + + if ($start == 1 && $res == 1) + return true; + + $query = 'INSERT INTO ' . $sequenceName + . ' (' . $seqcolName . ') VALUES (' . ($start - 1) . ')'; + + $res = $this->conn->exec($query); + + if ($res == 1) + return true; + + // Handle error + try { + $result = $this->conn->exec('DROP TABLE ' . $sequenceName); + } catch(IPF_ORM_Exception $e) { + throw new IPF_ORM_Exception('could not drop inconsistent sequence table'); + } + + + } + + public function createIndexSql($table, $name, array $definition) + { + $table = $table; + $name = $this->conn->formatter->getIndexName($name); + $name = $this->conn->quoteIdentifier($name); + $type = ''; + if (isset($definition['type'])) { + switch (strtolower($definition['type'])) { + case 'fulltext': + case 'unique': + $type = strtoupper($definition['type']) . ' '; + break; + default: + throw new IPF_ORM_Exception('Unknown index type ' . $definition['type']); + } + } + $query = 'CREATE ' . $type . 'INDEX ' . $name . ' ON ' . $table; + $query .= ' (' . $this->getIndexFieldDeclarationList($definition['fields']) . ')'; + + return $query; + } + + public function getDefaultFieldDeclaration($field) + { + $default = ''; + if (isset($field['default']) && ( ! isset($field['length']) || $field['length'] <= 255)) { + if ($field['default'] === '') { + $field['default'] = empty($field['notnull']) + ? null : $this->valid_default_values[$field['type']]; + + if ($field['default'] === '' + && ($this->conn->getAttribute(IPF_ORM::ATTR_PORTABILITY) & IPF_ORM::PORTABILITY_EMPTY_TO_NULL) + ) { + $field['default'] = ' '; + } + } + + // Proposed patch: + if ($field['type'] == 'enum' && $this->conn->getAttribute(IPF_ORM::ATTR_USE_NATIVE_ENUM)) { + $fieldType = 'varchar'; + } else { + $fieldType = $field['type']; + } + + $default = ' DEFAULT ' . $this->conn->quote($field['default'], $fieldType); + //$default = ' DEFAULT ' . $this->conn->quote($field['default'], $field['type']); + } + return $default; + } + + public function getIndexDeclaration($name, array $definition) + { + $name = $this->conn->formatter->getIndexName($name); + $type = ''; + if (isset($definition['type'])) { + switch (strtolower($definition['type'])) { + case 'fulltext': + case 'unique': + $type = strtoupper($definition['type']) . ' '; + break; + default: + throw new IPF_ORM_Exception('Unknown index type ' . $definition['type']); + } + } + + if ( ! isset($definition['fields'])) { + throw new IPF_ORM_Exception('No index columns given.'); + } + if ( ! is_array($definition['fields'])) { + $definition['fields'] = array($definition['fields']); + } + + $query = $type . 'INDEX ' . $this->conn->quoteIdentifier($name); + + $query .= ' (' . $this->getIndexFieldDeclarationList($definition['fields']) . ')'; + + return $query; + } + + public function getIndexFieldDeclarationList(array $fields) + { + $declFields = array(); + + foreach ($fields as $fieldName => $field) { + $fieldString = $this->conn->quoteIdentifier($fieldName); + + if (is_array($field)) { + if (isset($field['length'])) { + $fieldString .= '(' . $field['length'] . ')'; + } + + if (isset($field['sorting'])) { + $sort = strtoupper($field['sorting']); + switch ($sort) { + case 'ASC': + case 'DESC': + $fieldString .= ' ' . $sort; + break; + default: + throw new IPF_ORM_Exception('Unknown index sorting option given.'); + } + } + } else { + $fieldString = $this->conn->quoteIdentifier($field); + } + $declFields[] = $fieldString; + } + return implode(', ', $declFields); + } + + public function getAdvancedForeignKeyOptions(array $definition) + { + $query = ''; + if ( ! empty($definition['match'])) { + $query .= ' MATCH ' . $definition['match']; + } + if ( ! empty($definition['onUpdate'])) { + $query .= ' ON UPDATE ' . $this->getForeignKeyReferentialAction($definition['onUpdate']); + } + if ( ! empty($definition['onDelete'])) { + $query .= ' ON DELETE ' . $this->getForeignKeyReferentialAction($definition['onDelete']); + } + return $query; + } + + public function dropIndexSql($table, $name) + { + $table = $this->conn->quoteIdentifier($table, true); + $name = $this->conn->quoteIdentifier($this->conn->formatter->getIndexName($name), true); + return 'DROP INDEX ' . $name . ' ON ' . $table; + } + + public function dropTableSql($table) + { + $table = $this->conn->quoteIdentifier($table, true); + return 'DROP TABLE ' . $table; + } + + public function dropForeignKey($table, $name) + { + $table = $this->conn->quoteIdentifier($table); + $name = $this->conn->quoteIdentifier($name); + + return $this->conn->exec('ALTER TABLE ' . $table . ' DROP FOREIGN KEY ' . $name); + } +} \ No newline at end of file diff --git a/ipf/orm/formatter.php b/ipf/orm/formatter.php new file mode 100644 index 0000000..b01275d --- /dev/null +++ b/ipf/orm/formatter.php @@ -0,0 +1,132 @@ +string_quoting['escape_pattern']) { + return $text; + } + $tmp = $this->conn->string_quoting; + + $text = str_replace($tmp['escape_pattern'], + $tmp['escape_pattern'] . + $tmp['escape_pattern'], $text); + + foreach ($this->wildcards as $wildcard) { + $text = str_replace($wildcard, $tmp['escape_pattern'] . $wildcard, $text); + } + return $text; + } + + public function convertBooleans($item) + { + if (is_array($item)) { + foreach ($item as $k => $value) { + if (is_bool($value)) { + $item[$k] = (int) $value; + } + } + } else { + if (is_bool($item)) { + $item = (int) $item; + } + } + return $item; + } + + public function quoteIdentifier($str, $checkOption = true) + { + if ($checkOption && ! $this->conn->getAttribute(IPF_ORM::ATTR_QUOTE_IDENTIFIER)) { + return $str; + } + $tmp = $this->conn->identifier_quoting; + $str = str_replace($tmp['end'], + $tmp['escape'] . + $tmp['end'], $str); + + return $tmp['start'] . $str . $tmp['end']; + } + + public function quoteMultipleIdentifier($arr, $checkOption = true) + { + foreach ($arr as $k => $v) { + $arr[$k] = $this->quoteIdentifier($v, $checkOption); + } + + return $arr; + } + + public function quote($input, $type = null) + { + if ($type == null) { + $type = gettype($input); + } + switch ($type) { + case 'integer': + case 'enum': + case 'boolean': + case 'double': + case 'float': + case 'bool': + case 'decimal': + case 'int': + return $input; + case 'array': + case 'object': + $input = serialize($input); + case 'date': + case 'time': + case 'timestamp': + case 'string': + case 'char': + case 'varchar': + case 'text': + case 'gzip': + case 'blob': + case 'clob': + $this->conn->connect(); + + return $this->conn->getDbh()->quote($input); + } + } + + public function fixSequenceName($sqn) + { + $seqPattern = '/^'.preg_replace('/%s/', '([a-z0-9_]+)', $this->conn->getAttribute(IPF_ORM::ATTR_SEQNAME_FORMAT)).'$/i'; + $seqName = preg_replace($seqPattern, '\\1', $sqn); + + if ($seqName && ! strcasecmp($sqn, $this->getSequenceName($seqName))) { + return $seqName; + } + return $sqn; + } + + public function fixIndexName($idx) + { + $indexPattern = '/^'.preg_replace('/%s/', '([a-z0-9_]+)', $this->conn->getAttribute(IPF_ORM::ATTR_IDXNAME_FORMAT)).'$/i'; + $indexName = preg_replace($indexPattern, '\\1', $idx); + if ($indexName && ! strcasecmp($idx, $this->getIndexName($indexName))) { + return $indexName; + } + return $idx; + } + + public function getSequenceName($sqn) + { + return sprintf($this->conn->getAttribute(IPF_ORM::ATTR_SEQNAME_FORMAT), + preg_replace('/[^a-z0-9_\$.]/i', '_', $sqn)); + } + + public function getIndexName($idx) + { + return sprintf($this->conn->getAttribute(IPF_ORM::ATTR_IDXNAME_FORMAT), + preg_replace('/[^a-z0-9_\$]/i', '_', $idx)); + } + + public function getTableName($table) + { + return sprintf($this->conn->getAttribute(IPF_ORM::ATTR_TBLNAME_FORMAT), + $table); + } +} diff --git a/ipf/orm/hydrator.php b/ipf/orm/hydrator.php new file mode 100644 index 0000000..51c4fb0 --- /dev/null +++ b/ipf/orm/hydrator.php @@ -0,0 +1,274 @@ +_hydrationMode; + + $this->_tableAliases = $tableAliases; + + if ($hydrationMode == IPF_ORM::HYDRATE_NONE) { + return $stmt->fetchAll(PDO::FETCH_NUM); + } + + if ($hydrationMode == IPF_ORM::HYDRATE_ARRAY) { + $driver = new IPF_ORM_Hydrator_ArrayDriver(); + } else { + $driver = new IPF_ORM_Hydrator_RecordDriver(); + } + + // Used variables during hydration + reset($this->_queryComponents); + $rootAlias = key($this->_queryComponents); + $rootComponentName = $this->_queryComponents[$rootAlias]['table']->getComponentName(); + // if only one component is involved we can make our lives easier + $isSimpleQuery = count($this->_queryComponents) <= 1; + // Holds the resulting hydrated data structure + $result = array(); + // Holds hydration listeners that get called during hydration + $listeners = array(); + // Lookup map to quickly discover/lookup existing records in the result + $identifierMap = array(); + // Holds for each component the last previously seen element in the result set + $prev = array(); + // holds the values of the identifier/primary key fields of components, + // separated by a pipe '|' and grouped by component alias (r, u, i, ... whatever) + // the $idTemplate is a prepared template. $id is set to a fresh template when + // starting to process a row. + $id = array(); + $idTemplate = array(); + + $result = $driver->getElementCollection($rootComponentName); + + if ($stmt === false || $stmt === 0) { + return $result; + } + + // Initialize + foreach ($this->_queryComponents as $dqlAlias => $data) { + $componentName = $data['table']->getComponentName(); + $listeners[$componentName] = $data['table']->getRecordListener(); + $identifierMap[$dqlAlias] = array(); + $prev[$dqlAlias] = null; + $idTemplate[$dqlAlias] = ''; + } + + $event = new IPF_ORM_Event(null, IPF_ORM_Event::HYDRATE, null); + + // Process result set + $cache = array(); + while ($data = $stmt->fetch(IPF_ORM::FETCH_ASSOC)) { + $id = $idTemplate; // initialize the id-memory + $nonemptyComponents = array(); + $rowData = $this->_gatherRowData($data, $cache, $id, $nonemptyComponents); + + // + // hydrate the data of the root component from the current row + // + $table = $this->_queryComponents[$rootAlias]['table']; + $componentName = $table->getComponentName(); + // Ticket #1115 (getInvoker() should return the component that has addEventListener) + $event->setInvoker($table); + $event->set('data', $rowData[$rootAlias]); + $listeners[$componentName]->preHydrate($event); + + $index = false; + + // Check for an existing element + if ($isSimpleQuery || ! isset($identifierMap[$rootAlias][$id[$rootAlias]])) { + $element = $driver->getElement($rowData[$rootAlias], $componentName); + $event->set('data', $element); + $listeners[$componentName]->postHydrate($event); + + // do we need to index by a custom field? + if ($field = $this->_getCustomIndexField($rootAlias)) { + if (isset($result[$field])) { + throw new IPF_ORM_Exception("Couldn't hydrate. Found non-unique key mapping."); + } else if ( ! isset($element[$field])) { + throw new IPF_ORM_Exception("Couldn't hydrate. Found a non-existent key."); + } + $result[$element[$field]] = $element; + } else { + $result[] = $element; + } + + $identifierMap[$rootAlias][$id[$rootAlias]] = $driver->getLastKey($result); + } else { + $index = $identifierMap[$rootAlias][$id[$rootAlias]]; + } + + $this->_setLastElement($prev, $result, $index, $rootAlias, false); + unset($rowData[$rootAlias]); + + // end hydrate data of the root component for the current row + + + // $prev[$rootAlias] now points to the last element in $result. + // now hydrate the rest of the data found in the current row, that belongs to other + // (related) components. + foreach ($rowData as $dqlAlias => $data) { + $index = false; + $map = $this->_queryComponents[$dqlAlias]; + $table = $map['table']; + $componentName = $table->getComponentName(); + $event->set('data', $data); + $listeners[$componentName]->preHydrate($event); + + $parent = $map['parent']; + $relation = $map['relation']; + $relationAlias = $map['relation']->getAlias(); + + $path = $parent . '.' . $dqlAlias; + + if ( ! isset($prev[$parent])) { + unset($prev[$dqlAlias]); // Ticket #1228 + continue; + } + + // check the type of the relation + if ( ! $relation->isOneToOne() && $driver->initRelated($prev[$parent], $relationAlias)) { + $oneToOne = false; + // append element + if (isset($nonemptyComponents[$dqlAlias])) { + $indexExists = isset($identifierMap[$path][$id[$parent]][$id[$dqlAlias]]); + $index = $indexExists ? $identifierMap[$path][$id[$parent]][$id[$dqlAlias]] : false; + $indexIsValid = $index !== false ? isset($prev[$parent][$relationAlias][$index]) : false; + if ( ! $indexExists || ! $indexIsValid) { + $element = $driver->getElement($data, $componentName); + $event->set('data', $element); + $listeners[$componentName]->postHydrate($event); + + if ($field = $this->_getCustomIndexField($dqlAlias)) { + if (isset($prev[$parent][$relationAlias][$element[$field]])) { + throw new IPF_ORM_Exception("Couldn't hydrate. Found non-unique key mapping."); + } else if ( ! isset($element[$field])) { + throw new IPF_ORM_Exception("Couldn't hydrate. Found a non-existent key."); + } + $prev[$parent][$relationAlias][$element[$field]] = $element; + } else { + $prev[$parent][$relationAlias][] = $element; + } + $identifierMap[$path][$id[$parent]][$id[$dqlAlias]] = $driver->getLastKey($prev[$parent][$relationAlias]); + } + // register collection for later snapshots + $driver->registerCollection($prev[$parent][$relationAlias]); + } + } else { + // 1-1 relation + $oneToOne = true; + + if ( ! isset($nonemptyComponents[$dqlAlias]) && ! isset($prev[$parent][$relationAlias])) { + $prev[$parent][$relationAlias] = $driver->getNullPointer(); + } else if ( ! isset($prev[$parent][$relationAlias])) { + $element = $driver->getElement($data, $componentName); + + // [FIX] Tickets #1205 and #1237 + $event->set('data', $element); + $listeners[$componentName]->postHydrate($event); + + $prev[$parent][$relationAlias] = $element; + } + } + + $coll =& $prev[$parent][$relationAlias]; + $this->_setLastElement($prev, $coll, $index, $dqlAlias, $oneToOne); + } + } + + $stmt->closeCursor(); + $driver->flush(); + //$e = microtime(true); + //echo 'Hydration took: ' . ($e - $s) . ' for '.count($result).' records
    '; + + return $result; + } + + protected function _setLastElement(&$prev, &$coll, $index, $dqlAlias, $oneToOne) + { + if ($coll === self::$_null || $coll === null) { + unset($prev[$dqlAlias]); // Ticket #1228 + return; + } + + if ($index !== false) { + // Link element at $index to previous element for the component + // identified by the DQL alias $alias + $prev[$dqlAlias] =& $coll[$index]; + return; + } + + if (is_array($coll) && $coll) { + if ($oneToOne) { + $prev[$dqlAlias] =& $coll; + } else { + end($coll); + $prev[$dqlAlias] =& $coll[key($coll)]; + } + } else if (count($coll) > 0) { + $prev[$dqlAlias] = $coll->getLast(); + } + } + + protected function _gatherRowData(&$data, &$cache, &$id, &$nonemptyComponents) + { + $rowData = array(); + + foreach ($data as $key => $value) { + // Parse each column name only once. Cache the results. + if ( ! isset($cache[$key])) { + // check ignored names. fastest solution for now. if we get more we'll start + // to introduce a list. + if ($key == 'IPF_ORM_ROWNUM') continue; + $e = explode('__', $key); + $last = strtolower(array_pop($e)); + $cache[$key]['dqlAlias'] = $this->_tableAliases[strtolower(implode('__', $e))]; + $table = $this->_queryComponents[$cache[$key]['dqlAlias']]['table']; + $fieldName = $table->getFieldName($last); + $cache[$key]['fieldName'] = $fieldName; + if ($table->isIdentifier($fieldName)) { + $cache[$key]['isIdentifier'] = true; + } else { + $cache[$key]['isIdentifier'] = false; + } + $type = $table->getTypeOfColumn($last); + if ($type == 'integer' || $type == 'string') { + $cache[$key]['isSimpleType'] = true; + } else { + $cache[$key]['type'] = $type; + $cache[$key]['isSimpleType'] = false; + } + } + + $map = $this->_queryComponents[$cache[$key]['dqlAlias']]; + $table = $map['table']; + $dqlAlias = $cache[$key]['dqlAlias']; + $fieldName = $cache[$key]['fieldName']; + if (isset($this->_queryComponents[$dqlAlias]['agg'][$fieldName])) { + $fieldName = $this->_queryComponents[$dqlAlias]['agg'][$fieldName]; + } + + if ($cache[$key]['isIdentifier']) { + $id[$dqlAlias] .= '|' . $value; + } + + if ($cache[$key]['isSimpleType']) { + $rowData[$dqlAlias][$fieldName] = $value; + } else { + $rowData[$dqlAlias][$fieldName] = $table->prepareValue( + $fieldName, $value, $cache[$key]['type']); + } + + if ( ! isset($nonemptyComponents[$dqlAlias]) && $value !== null) { + $nonemptyComponents[$dqlAlias] = true; + } + } + + return $rowData; + } + + protected function _getCustomIndexField($alias) + { + return isset($this->_queryComponents[$alias]['map']) ? $this->_queryComponents[$alias]['map'] : null; + } +} \ No newline at end of file diff --git a/ipf/orm/hydrator/abstract.php b/ipf/orm/hydrator/abstract.php new file mode 100644 index 0000000..47d71aa --- /dev/null +++ b/ipf/orm/hydrator/abstract.php @@ -0,0 +1,32 @@ +_hydrationMode = $hydrationMode; + } + + public function getHydrationMode() + { + return $this->_hydrationMode; + } + + public function setQueryComponents(array $queryComponents) + { + $this->_queryComponents = $queryComponents; + } + + public function getQueryComponents() + { + return $this->_queryComponents; + } + + abstract public function hydrateResultSet($stmt, $tableAliases); +} \ No newline at end of file diff --git a/ipf/orm/hydrator/arraydriver.php b/ipf/orm/hydrator/arraydriver.php new file mode 100644 index 0000000..6018d62 --- /dev/null +++ b/ipf/orm/hydrator/arraydriver.php @@ -0,0 +1,40 @@ +_collections[] = $coll; + + return $coll; + } + + public function getLastKey($coll) + { + $coll->end(); + + return $coll->key(); + } + + public function initRelated(IPF_ORM_Record $record, $name) + { + if ( ! isset($this->_initializedRelations[$record->getOid()][$name])) { + $relation = $record->getTable()->getRelation($name); + $coll = new IPF_ORM_Collection($relation->getTable()->getComponentName()); + $coll->setReference($record, $relation); + $record[$name] = $coll; + $this->_initializedRelations[$record->getOid()][$name] = true; + } + return true; + } + + public function registerCollection(IPF_ORM_Collection $coll) + { + $this->_collections[] = $coll; + } + + public function getNullPointer() + { + return self::$_null; + } + + public function getElement(array $data, $component) + { + $component = $this->_getClassNameToReturn($data, $component); + if ( ! isset($this->_tables[$component])) { + $this->_tables[$component] = IPF_ORM::getTable($component); + $this->_tables[$component]->setAttribute(IPF_ORM::ATTR_LOAD_REFERENCES, false); + } + + $this->_tables[$component]->setData($data); + $record = $this->_tables[$component]->getRecord(); + + return $record; + } + + public function flush() + { + // take snapshots from all initialized collections + foreach ($this->_collections as $key => $coll) { + $coll->takeSnapshot(); + } + foreach ($this->_tables as $table) { + $table->setAttribute(IPF_ORM::ATTR_LOAD_REFERENCES, true); + } + $this->_initializedRelations = null; + $this->_collections = null; + $this->_tables = null; + } + + protected function _getClassnameToReturn(array &$data, $component) + { + if ( ! isset($this->_tables[$component])) { + $this->_tables[$component] = IPF_ORM::getTable($component); + $this->_tables[$component]->setAttribute(IPF_ORM::ATTR_LOAD_REFERENCES, false); + } + + if ( ! ($subclasses = $this->_tables[$component]->getOption('subclasses'))) { + return $component; + } + + foreach ($subclasses as $subclass) { + $table = IPF_ORM::getTable($subclass); + $inheritanceMap = $table->getOption('inheritanceMap'); + list($key, $value) = each($inheritanceMap); + if ( ! isset($data[$key]) || $data[$key] != $value) { + continue; + } else { + return $table->getComponentName(); + } + } + return $component; + } +} diff --git a/ipf/orm/import/builder.php b/ipf/orm/import/builder.php new file mode 100644 index 0000000..34d683f --- /dev/null +++ b/ipf/orm/import/builder.php @@ -0,0 +1,752 @@ +loadTemplate(); + } + + public function setTargetPath($path) + { + if ($path) { + if ( ! $this->_packagesPath) { + $this->setPackagesPath($path . DIRECTORY_SEPARATOR . $this->_packagesFolderName); + } + + $this->_path = $path; + } + } + + public function setPackagesPrefix($packagesPrefix) + { + $this->_packagesPrefix = $packagesPrefix; + } + + public function setPackagesPath($packagesPath) + { + if ($packagesPath) { + $this->_packagesPath = $packagesPath; + } + } + + public function generateBaseClasses($bool = null) + { + if ($bool !== null) { + $this->_generateBaseClasses = $bool; + } + + return $this->_generateBaseClasses; + } + + public function generateTableClasses($bool = null) + { + if ($bool !== null) { + $this->_generateTableClasses = $bool; + } + + return $this->_generateTableClasses; + } + + public function generateAccessors($bool = null) + { + if ($bool !== null) { + $this->_generateAccessors = $bool; + } + return $this->_generateAccessors; + } + + public function setBaseClassPrefix($prefix) + { + $this->_baseClassPrefix = $prefix; + } + + public function getBaseClassPrefix() + { + return $this->_baseClassPrefix; + } + + public function setBaseClassesDirectory($baseClassesDirectory) + { + $this->_baseClassesDirectory = $baseClassesDirectory; + } + + public function setBaseClassName($className) + { + $this->_baseClassName = $className; + } + + public function setSuffix($suffix) + { + $this->_suffix = $suffix; + } + + public function getTargetPath() + { + return $this->_path; + } + + public function setOptions($options) + { + if ( ! empty($options)) { + foreach ($options as $key => $value) { + $this->setOption($key, $value); + } + } + } + + public function setOption($key, $value) + { + $name = 'set' . IPF_ORM_Inflector::classify($key); + + if (method_exists($this, $name)) { + $this->$name($value); + } else { + $key = '_' . $key; + $this->$key = $value; + } + } + + public function loadTemplate() + { + if (isset(self::$_tpl)) { + return; + } + + self::$_tpl = '/**' . PHP_EOL + . ' * This class has been auto-generated by the IPF ORM Framework' . PHP_EOL + . ' */' . PHP_EOL + . '%sclass %s extends %s' . PHP_EOL + . '{' + . '%s' . PHP_EOL + . '%s' . PHP_EOL + . '%s' + . '}'; + } + + public function buildTableDefinition(array $definition) + { + if (isset($definition['inheritance']['type']) && ($definition['inheritance']['type'] == 'simple' || $definition['inheritance']['type'] == 'column_aggregation')) { + return; + } + + $ret = array(); + + $i = 0; + + if (isset($definition['inheritance']['type']) && $definition['inheritance']['type'] == 'concrete') { + $ret[$i] = " parent::setTableDefinition();"; + $i++; + } + + if (isset($definition['tableName']) && !empty($definition['tableName'])) { + $ret[$i] = " ".'$this->setTableName(\''. $definition['tableName'].'\');'; + $i++; + } + + if (isset($definition['columns']) && is_array($definition['columns']) && !empty($definition['columns'])) { + $ret[$i] = $this->buildColumns($definition['columns']); + $i++; + } + + if (isset($definition['indexes']) && is_array($definition['indexes']) && !empty($definition['indexes'])) { + $ret[$i] = $this->buildIndexes($definition['indexes']); + $i++; + } + + if (isset($definition['attributes']) && is_array($definition['attributes']) && !empty($definition['attributes'])) { + $ret[$i] = $this->buildAttributes($definition['attributes']); + $i++; + } + + if (isset($definition['options']) && is_array($definition['options']) && !empty($definition['options'])) { + $ret[$i] = $this->buildOptions($definition['options']); + $i++; + } + + if (isset($definition['inheritance']['subclasses']) && ! empty($definition['inheritance']['subclasses'])) { + $ret[$i] = " ".'$this->setSubClasses('. $this->varExport($definition['inheritance']['subclasses']).');'; + $i++; + } + + $code = implode(PHP_EOL, $ret); + $code = trim($code); + + return PHP_EOL . " public function setTableDefinition()" . PHP_EOL . ' {' . PHP_EOL . ' ' . $code . PHP_EOL . ' }'; + } + + public function buildSetUp(array $definition) + { + $ret = array(); + $i = 0; + + if (isset($definition['relations']) && is_array($definition['relations']) && ! empty($definition['relations'])) { + foreach ($definition['relations'] as $name => $relation) { + $class = isset($relation['class']) ? $relation['class']:$name; + $alias = (isset($relation['alias']) && $relation['alias'] !== $relation['class']) ? ' as ' . $relation['alias'] : ''; + + if ( ! isset($relation['type'])) { + $relation['type'] = IPF_ORM_Relation::ONE; + } + + if ($relation['type'] === IPF_ORM_Relation::ONE || + $relation['type'] === IPF_ORM_Relation::ONE_COMPOSITE) { + $ret[$i] = " ".'$this->hasOne(\'' . $class . $alias . '\''; + } else { + $ret[$i] = " ".'$this->hasMany(\'' . $class . $alias . '\''; + } + + $a = array(); + + if (isset($relation['refClass'])) { + $a[] = '\'refClass\' => ' . $this->varExport($relation['refClass']); + } + + if (isset($relation['deferred']) && $relation['deferred']) { + $a[] = '\'default\' => ' . $this->varExport($relation['deferred']); + } + + if (isset($relation['local']) && $relation['local']) { + $a[] = '\'local\' => ' . $this->varExport($relation['local']); + } + + if (isset($relation['foreign']) && $relation['foreign']) { + $a[] = '\'foreign\' => ' . $this->varExport($relation['foreign']); + } + + if (isset($relation['onDelete']) && $relation['onDelete']) { + $a[] = '\'onDelete\' => ' . $this->varExport($relation['onDelete']); + } + + if (isset($relation['onUpdate']) && $relation['onUpdate']) { + $a[] = '\'onUpdate\' => ' . $this->varExport($relation['onUpdate']); + } + + if (isset($relation['equal']) && $relation['equal']) { + $a[] = '\'equal\' => ' . $this->varExport($relation['equal']); + } + + if (isset($relation['owningSide']) && $relation['owningSide']) { + $a[] = '\'owningSide\' => ' . $this->varExport($relation['owningSide']); + } + + if ( ! empty($a)) { + $ret[$i] .= ', ' . 'array('; + $length = strlen($ret[$i]); + $ret[$i] .= implode(',' . PHP_EOL . str_repeat(' ', $length), $a) . ')'; + } + + $ret[$i] .= ');'.PHP_EOL; + $i++; + } + } + + if (isset($definition['templates']) && is_array($definition['templates']) && !empty($definition['templates'])) { + $ret[$i] = $this->buildTemplates($definition['templates']); + $i++; + } + + if (isset($definition['actAs']) && is_array($definition['actAs']) && !empty($definition['actAs'])) { + $ret[$i] = $this->buildActAs($definition['actAs']); + $i++; + } + + if (isset($definition['listeners']) && is_array($definition['listeners']) && !empty($definition['listeners'])) { + foreach($definition['listeners'] as $listener) + { + $ret[$i] = $this->buildListener($listener); + $i++; + } + } + + $code = implode(PHP_EOL, $ret); + $code = trim($code); + + // If the body of the function has contents then we need to + if ($code) { + // If the body of the function has contents and we are using inheritance + // then we need call the parent::setUp() before the body of the function + // Class table inheritance is the only one we shouldn't call parent::setUp() for + if ($code && isset($definition['inheritance']['type']) && $definition['inheritance']['type'] != 'class_table') { + $code = "parent::setUp();" . PHP_EOL . ' ' . $code; + } + } + + // If we have some code for the function then lets define it and return it + if ($code) { + return ' public function setUp()' . PHP_EOL . ' {' . PHP_EOL . ' ' . $code . PHP_EOL . ' }'; + } + } + + public function buildColumns(array $columns) + { + $build = null; + foreach ($columns as $name => $column) { + $columnName = isset($column['name']) ? $column['name']:$name; + $build .= " ".'$this->hasColumn(\'' . $columnName . '\', \'' . $column['type'] . '\''; + + if ($column['length']) { + $build .= ', ' . $column['length']; + } else { + $build .= ', null'; + } + + $options = $column; + + // Remove name, alltypes, ntype. They are not needed in options array + unset($options['name']); + unset($options['alltypes']); + unset($options['ntype']); + + // Remove notnull => true if the column is primary + // Primary columns are implied to be notnull in IPF + if (isset($options['primary']) && $options['primary'] == true && (isset($options['notnull']) && $options['notnull'] == true)) { + unset($options['notnull']); + } + + // Remove default if the value is 0 and the column is a primary key + // IPF defaults to 0 if it is a primary key + if (isset($options['primary']) && $options['primary'] == true && (isset($options['default']) && $options['default'] == 0)) { + unset($options['default']); + } + + // These can be removed if they are empty. They all default to a false/0/null value anyways + $remove = array('fixed', 'primary', 'notnull', 'autoincrement', 'unsigned'); + foreach ($remove as $key) { + if (isset($options[$key]) && empty($options[$key])) { + unset($options[$key]); + } + } + + // Remove null and empty array values + foreach ($options as $key => $value) { + if (is_null($value) || (is_array($value) && empty($value))) { + unset($options[$key]); + } + } + + if (is_array($options) && !empty($options)) { + $build .= ', ' . $this->varExport($options); + } + + $build .= ');' . PHP_EOL; + } + + return $build; + } + + public function buildAccessors(array $definition) + { + $accessors = array(); + foreach (array_keys($definition['columns']) as $name) { + $accessors[] = $name; + } + + foreach ($definition['relations'] as $relation) { + $accessors[] = $relation['alias']; + } + + $ret = ''; + foreach ($accessors as $name) { + // getters + $ret .= PHP_EOL . ' public function get' . IPF_ORM_Inflector::classify(IPF_ORM_Inflector::tableize($name)) . "(\$load = true)" . PHP_EOL; + $ret .= " {" . PHP_EOL; + $ret .= " return \$this->get('{$name}', \$load);" . PHP_EOL; + $ret .= " }" . PHP_EOL; + + // setters + $ret .= PHP_EOL . ' public function set' . IPF_ORM_Inflector::classify(IPF_ORM_Inflector::tableize($name)) . "(\${$name}, \$load = true)" . PHP_EOL; + $ret .= " {" . PHP_EOL; + $ret .= " return \$this->set('{$name}', \${$name}, \$load);" . PHP_EOL; + $ret .= " }" . PHP_EOL; + } + + return $ret; + } + + public function buildTemplates(array $templates) + { + $build = ''; + foreach ($templates as $name => $options) { + + if (is_array($options) && !empty($options)) { + $optionsPhp = $this->varExport($options); + + $build .= " \$this->loadTemplate('" . $name . "', " . $optionsPhp . ");" . PHP_EOL; + } else { + if (isset($templates[0])) { + $build .= " \$this->loadTemplate('" . $options . "');" . PHP_EOL; + } else { + $build .= " \$this->loadTemplate('" . $name . "');" . PHP_EOL; + } + } + } + + return $build; + } + + private function emitAssign($level, $name, $option) + { + // find class matching $name + $classname = $name; + if (class_exists("IPF_ORM_Template_$name", true)) { + $classname = "IPF_ORM_Template_$name"; + } + return " \$" . strtolower($name) . "$level = new $classname($option);". PHP_EOL; + } + + private function emitAddChild($level, $parent, $name) + { + return " \$" . strtolower($parent) . ($level - 1) . "->addChild(\$" . strtolower($name) . "$level);" . PHP_EOL; + } + + private function emitActAs($level, $name) + { + return " \$this->actAs(\$" . strtolower($name) . "$level);" . PHP_EOL; + } + + public function buildActAs($actAs) + { + $emittedActAs = array(); + $build = $this->innerBuildActAs($actAs, 0, null, $emittedActAs); + foreach($emittedActAs as $str) { + $build .= $str; + } + return $build; + } + + private function innerBuildActAs($actAs, $level = 0, $parent = null, array &$emittedActAs) + { + // rewrite special case of actAs: [Behavior] which gave [0] => Behavior + if(is_array($actAs) && isset($actAs[0]) && !is_array($actAs[0])) { + $actAs = array_flip($actAs); + } + + $build = ''; + $currentParent = $parent; + if(is_array($actAs)) { + foreach($actAs as $template => $options) { + if ($template == 'actAs') { + // found another actAs + $build .= $this->innerBuildActAs($options, $level + 1, $parent, $emittedActAs); + } else if (is_array($options)) { + // remove actAs from options + $realOptions = array(); + $leftActAs = array(); + foreach($options as $name => $value) { + if ($name != 'actAs') { + $realOptions[$name] = $options[$name]; + } else { + $leftActAs[$name] = $options[$name]; + } + } + + $optionPHP = $this->varExport($realOptions); + $build .= $this->emitAssign($level, $template, $optionPHP); + if ($level == 0) { + $emittedActAs[] = $this->emitActAs($level, $template); + } else { + $build .= $this->emitAddChild($level, $currentParent, $template); + } + // descend for the remainings actAs + $parent = $template; + $build .= $this->innerBuildActAs($leftActAs, $level, $template, $emittedActAs); + } else { + $build .= $this->emitAssign($level, $template, null); + if ($level == 0) { + $emittedActAs[] = $this->emitActAs($level, $template); + } else { + $build .= $this->emitAddChild($level, $currentParent, $template); + } + $parent = $template; + } + } + } else { + $build .= $this->emitAssign($level, $actAs, null); + if ($level == 0) { + $emittedActAs[] = $this->emitActAs($level, $actAs); + } else { + $build .= $this->emitAddChild($level, $currentParent, $actAs); + } + } + + return $build; + } + + public function buildListener($listener) + { + return PHP_EOL." ".'$this->addListener(new '.$listener.'());'; + } + + public function buildAttributes(array $attributes) + { + $build = PHP_EOL; + foreach ($attributes as $key => $value) { + + if (is_bool($value)) + { + $values = $value ? 'true':'false'; + } else { + if ( ! is_array($value)) { + $value = array($value); + } + + $values = ''; + foreach ($value as $attr) { + $values .= "IPF_ORM::" . strtoupper($key) . "_" . strtoupper($attr) . ' ^ '; + } + + // Trim last ^ + $values = substr($values, 0, strlen($values) - 3); + } + + $build .= " \$this->setAttribute(IPF_ORM::ATTR_" . strtoupper($key) . ", " . $values . ");" . PHP_EOL; + } + + return $build; + } + + public function buildOptions(array $options) + { + $build = ''; + foreach ($options as $name => $value) { + $build .= " \$this->option('$name', " . $this->varExport($value) . ");" . PHP_EOL; + } + + return $build; + } + + public function buildIndexes(array $indexes) + { + $build = ''; + + foreach ($indexes as $indexName => $definitions) { + $build .= PHP_EOL . " \$this->index('" . $indexName . "'"; + $build .= ', ' . $this->varExport($definitions); + $build .= ');'; + } + + return $build; + } + + public function buildDefinition(array $definition) + { + if ( ! isset($definition['className'])) { + throw new IPF_ORM_Exception('Missing class name.'); + } + + $abstract = isset($definition['abstract']) && $definition['abstract'] === true ? 'abstract ':null; + $className = $definition['className']; + $extends = isset($definition['inheritance']['extends']) ? $definition['inheritance']['extends']:$this->_baseClassName; + + if ( ! (isset($definition['no_definition']) && $definition['no_definition'] === true)) { + $tableDefinitionCode = $this->buildTableDefinition($definition); + $setUpCode = $this->buildSetUp($definition); + } else { + $tableDefinitionCode = null; + $setUpCode = null; + } + + if ($tableDefinitionCode && $setUpCode) { + $setUpCode = PHP_EOL . $setUpCode; + } + + if ( ! isset($definition['generate_accessors']) || !$definition['generate_accessors']) { + $definition['generate_accessors'] = $this->generateAccessors(); + } + + $accessorsCode = (isset($definition['generate_accessors']) && $definition['generate_accessors'] === true) ? $this->buildAccessors($definition):null; + + $content = sprintf(self::$_tpl, $abstract, + $className, + $extends, + $tableDefinitionCode, + $setUpCode, + $accessorsCode); + + return $content; + } + + public function buildRecord(array $definition) + { + if ( ! isset($definition['className'])) { + throw new IPF_ORM_Exception('Missing class name.'); + } + + $definition['topLevelClassName'] = $definition['className']; + + if ($this->generateBaseClasses()) { + $definition['is_package'] = (isset($definition['package']) && $definition['package']) ? true:false; + + if ($definition['is_package']) { + $e = explode('.', trim($definition['package'])); + $definition['package_name'] = $e[0]; + + $definition['package_path'] = ! empty($e) ? implode(DIRECTORY_SEPARATOR, $e):$definition['package_name']; + } + // Top level definition that extends from all the others + $topLevel = $definition; + unset($topLevel['tableName']); + + // If we have a package then we need to make this extend the package definition and not the base definition + // The package definition will then extends the base definition + $topLevel['inheritance']['extends'] = (isset($topLevel['package']) && $topLevel['package']) ? $this->_packagesPrefix . $topLevel['className']:$this->_baseClassPrefix . $topLevel['className']; + $topLevel['no_definition'] = true; + $topLevel['generate_once'] = true; + $topLevel['is_main_class'] = true; + unset($topLevel['connection']); + + // Package level definition that extends from the base definition + if (isset($definition['package'])) { + + $packageLevel = $definition; + $packageLevel['className'] = $topLevel['inheritance']['extends']; + $packageLevel['inheritance']['extends'] = $this->_baseClassPrefix . $topLevel['className']; + $packageLevel['no_definition'] = true; + $packageLevel['abstract'] = true; + $packageLevel['override_parent'] = true; + $packageLevel['generate_once'] = true; + $packageLevel['is_package_class'] = true; + unset($packageLevel['connection']); + + $packageLevel['tableClassName'] = $packageLevel['className'] . 'Table'; + $packageLevel['inheritance']['tableExtends'] = isset($definition['inheritance']['extends']) ? $definition['inheritance']['extends'] . 'Table':'IPF_ORM_Table'; + + $topLevel['tableClassName'] = $topLevel['topLevelClassName'] . 'Table'; + $topLevel['inheritance']['tableExtends'] = $packageLevel['className'] . 'Table'; + } else { + $topLevel['tableClassName'] = $topLevel['className'] . 'Table'; + $topLevel['inheritance']['tableExtends'] = isset($definition['inheritance']['extends']) ? $definition['inheritance']['extends'] . 'Table':'IPF_ORM_Table'; + } + + $baseClass = $definition; + $baseClass['className'] = $this->_baseClassPrefix . $baseClass['className']; + $baseClass['abstract'] = true; + $baseClass['override_parent'] = false; + $baseClass['is_base_class'] = true; + + $this->writeDefinition($baseClass); + + if ( ! empty($packageLevel)) { + $this->writeDefinition($packageLevel); + } + + $this->writeDefinition($topLevel); + } else { + $this->writeDefinition($definition); + } + } + + public function writeTableDefinition($className, $path, $options = array()) + { + $content = '_suffix; + + IPF_ORM::loadModel($className, $writePath); + + if ( ! file_exists($writePath)) { + file_put_contents($writePath, $content); + } + } + + public function writeDefinition(array $definition) + { + $definitionCode = $this->buildDefinition($definition); + + $fileName = $definition['className'] . $this->_suffix; + + $packagesPath = $this->_packagesPath ? $this->_packagesPath:$this->_path; + + // If this is a main class that either extends from Base or Package class + if (isset($definition['is_main_class']) && $definition['is_main_class']) { + // If is package then we need to put it in a package subfolder + if (isset($definition['is_package']) && $definition['is_package']) { + $writePath = $this->_path . DIRECTORY_SEPARATOR . $definition['package_name']; + // Otherwise lets just put it in the root of the path + } else { + $writePath = $this->_path; + } + + if ($this->generateTableClasses()) { + $this->writeTableDefinition($definition['tableClassName'], $writePath, array('extends' => $definition['inheritance']['tableExtends'])); + } + } + // If is the package class then we need to make the path to the complete package + else if (isset($definition['is_package_class']) && $definition['is_package_class']) { + $writePath = $packagesPath . DIRECTORY_SEPARATOR . $definition['package_path']; + + if ($this->generateTableClasses()) { + $this->writeTableDefinition($definition['tableClassName'], $writePath, array('extends' => $definition['inheritance']['tableExtends'])); + } + } + // If it is the base class of the ipf record definition + else if (isset($definition['is_base_class']) && $definition['is_base_class']) { + // If it is a part of a package then we need to put it in a package subfolder + if (isset($definition['is_package']) && $definition['is_package']) { + $basePath = $this->_path . DIRECTORY_SEPARATOR . $definition['package_name']; + $writePath = $basePath . DIRECTORY_SEPARATOR . $this->_baseClassesDirectory; + // Otherwise lets just put it in the root generated folder + } else { + $writePath = $this->_path . DIRECTORY_SEPARATOR . $this->_baseClassesDirectory; + } + } + + // If we have a writePath from the if else conditionals above then use it + if (isset($writePath)) { + IPF_ORM_Utils::makeDirectories($writePath); + + $writePath .= DIRECTORY_SEPARATOR . $fileName; + // Otherwise none of the conditions were met and we aren't generating base classes + } else { + IPF_ORM_Utils::makeDirectories($this->_path); + + $writePath = $this->_path . DIRECTORY_SEPARATOR . $fileName; + } + + $code = "bindComponent('" . $definition['connectionClassName'] . "', '" . $definition['connection'] . "');" . PHP_EOL; + } + + $code .= PHP_EOL . $definitionCode; + + if (isset($definition['generate_once']) && $definition['generate_once'] === true) { + if ( ! file_exists($writePath)) { + $bytes = file_put_contents($writePath, $code); + } + } else { + $bytes = file_put_contents($writePath, $code); + } + + if (isset($bytes) && $bytes === false) { + throw new IPF_ORM_Exception("Couldn't write file " . $writePath); + } + + IPF_ORM::loadModel($definition['className'], $writePath); + } +} \ No newline at end of file diff --git a/ipf/orm/import/schema.php b/ipf/orm/import/schema.php new file mode 100644 index 0000000..b8c64aa --- /dev/null +++ b/ipf/orm/import/schema.php @@ -0,0 +1,542 @@ + 'Package', + 'packagesPath' => '', + 'packagesFolderName' => 'packages', + 'suffix' => '.php', + 'generateBaseClasses' => true, + 'generateTableClasses' => false, + 'generateAccessors' => false, + 'baseClassesPrefix' => 'Base', + 'baseClassesDirectory' => '_generated', + 'baseClassName' => 'IPF_ORM_Record'); + + protected $_validation = array('root' => array('abstract', + 'connection', + 'className', + 'tableName', + 'connection', + 'relations', + 'columns', + 'indexes', + 'attributes', + 'templates', + 'actAs', + 'options', + 'package', + 'inheritance', + 'detect_relations', + 'generate_accessors', + 'listeners'), + + 'column' => array('name', + 'format', + 'fixed', + 'primary', + 'autoincrement', + 'type', + 'length', + 'size', + 'default', + 'scale', + 'values', + 'comment', + 'sequence', + 'protected', + 'zerofill', + 'owner'), + + 'relation' => array('key', + 'class', + 'alias', + 'type', + 'refClass', + 'local', + 'foreign', + 'foreignClass', + 'foreignAlias', + 'foreignType', + 'autoComplete', + 'onDelete', + 'onUpdate', + 'equal', + 'owningSide'), + + 'inheritance'=> array('type', + 'extends', + 'keyField', + 'keyValue')); + + protected $_validators = array(); + public function getValidators() + { + if (empty($this->_validators)) { + $this->_validators = IPF_ORM_Utils::getValidators(); + } + + return $this->_validators; + } + + public function getOption($name) + { + if (isset($this->_options[$name])) { + return $this->_options[$name]; + } + } + + public function getOptions() + { + return $this->_options; + } + + public function setOption($name, $value) + { + if (isset($this->_options[$name])) { + $this->_options[$name] = $value; + } + } + + public function setOptions($options) + { + if ( ! empty($options)) { + $this->_options = $options; + } + } + + public function buildSchema($schema, $format) + { + $array = array(); + + foreach ((array) $schema AS $s) { + if (is_file($s)) { + $array = array_merge($array, $this->parseSchema($s, $format)); + } else if (is_dir($s)) { + $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($s), + RecursiveIteratorIterator::LEAVES_ONLY); + + foreach ($it as $file) { + $e = explode('.', $file->getFileName()); + if (end($e) === $format) { + $array = array_merge($array, $this->parseSchema($file->getPathName(), $format)); + } + } + } else { + $array = array_merge($array, $this->parseSchema($s, $format)); + } + } + + $array = $this->_buildRelationships($array); + $array = $this->_processInheritance($array); + + return $array; + } + + public function importSchema($schema, $format = 'yml', $directory = null, $models = array()) + { + $builder = new IPF_ORM_Import_Builder(); + $builder->setTargetPath($directory); + $builder->setOptions($this->getOptions()); + + $array = $this->buildSchema($schema, $format); + + foreach ($array as $name => $definition) { + if ( ! empty($models) && !in_array($definition['className'], $models)) { + continue; + } + print " $name\n"; + $builder->buildRecord($definition); + } + } + + public function parseSchema($schema, $type) + { + $defaults = array('abstract' => false, + 'className' => null, + 'tableName' => null, + 'connection' => null, + 'relations' => array(), + 'indexes' => array(), + 'attributes' => array(), + 'templates' => array(), + 'actAs' => array(), + 'options' => array(), + 'package' => null, + 'inheritance' => array(), + 'detect_relations' => false, + 'generate_accessors' => false); + + $array = IPF_ORM_Parser::load($schema, $type); + + // Go through the schema and look for global values so we can assign them to each table/class + $globals = array(); + $globalKeys = array('connection', + 'attributes', + 'templates', + 'actAs', + 'options', + 'package', + 'inheritance', + 'detect_relations', + 'generate_accessors'); + + // Loop over and build up all the global values and remove them from the array + foreach ($array as $key => $value) { + if (in_array($key, $globalKeys)) { + unset($array[$key]); + $globals[$key] = $value; + } + } + + // Apply the globals to each table if it does not have a custom value set already + foreach ($array as $className => $table) { + foreach ($globals as $key => $value) { + if (!isset($array[$className][$key])) { + $array[$className][$key] = $value; + } + } + } + + $build = array(); + + foreach ($array as $className => $table) { + $this->_validateSchemaElement('root', array_keys($table), $className); + + $columns = array(); + + $className = isset($table['className']) ? (string) $table['className']:(string) $className; + + if (isset($table['inheritance']['keyField']) || isset($table['inheritance']['keyValue'])) { + $table['inheritance']['type'] = 'column_aggregation'; + } + + if (isset($table['tableName']) && $table['tableName']) { + $tableName = $table['tableName']; + } else { + if (isset($table['inheritance']['type']) && ($table['inheritance']['type'] == 'column_aggregation')) { + $tableName = null; + } else { + $tableName = IPF_ORM_Inflector::tableize($className); + } + } + + $connection = isset($table['connection']) ? $table['connection']:'current'; + + $columns = isset($table['columns']) ? $table['columns']:array(); + + if ( ! empty($columns)) { + foreach ($columns as $columnName => $field) { + + // Support short syntax: my_column: integer(4) + if ( ! is_array($field)) { + $original = $field; + $field = array(); + $field['type'] = $original; + } + + $colDesc = array(); + if (isset($field['name'])) { + $colDesc['name'] = $field['name']; + } else { + $colDesc['name'] = $columnName; + } + + $this->_validateSchemaElement('column', array_keys($field), $className . '->columns->' . $colDesc['name']); + + // Support short type(length) syntax: my_column: { type: integer(4) } + $e = explode('(', $field['type']); + if (isset($e[0]) && isset($e[1])) { + $colDesc['type'] = $e[0]; + $colDesc['length'] = substr($e[1], 0, strlen($e[1]) - 1); + } else { + $colDesc['type'] = isset($field['type']) ? (string) $field['type']:null; + $colDesc['length'] = isset($field['length']) ? (int) $field['length']:null; + $colDesc['length'] = isset($field['size']) ? (int) $field['size']:$colDesc['length']; + } + + $colDesc['fixed'] = isset($field['fixed']) ? (int) $field['fixed']:null; + $colDesc['primary'] = isset($field['primary']) ? (bool) (isset($field['primary']) && $field['primary']):null; + $colDesc['default'] = isset($field['default']) ? $field['default']:null; + $colDesc['autoincrement'] = isset($field['autoincrement']) ? (bool) (isset($field['autoincrement']) && $field['autoincrement']):null; + $colDesc['sequence'] = isset($field['sequence']) ? (string) $field['sequence']:null; + $colDesc['values'] = isset($field['values']) ? (array) $field['values']:null; + + // Include all the specified and valid validators in the colDesc + $validators = $this->getValidators(); + + foreach ($validators as $validator) { + if (isset($field[$validator])) { + $colDesc[$validator] = $field[$validator]; + } + } + + $columns[(string) $columnName] = $colDesc; + } + } + + // Apply the default values + foreach ($defaults as $key => $defaultValue) { + if (isset($table[$key]) && ! isset($build[$className][$key])) { + $build[$className][$key] = $table[$key]; + } else { + $build[$className][$key] = isset($build[$className][$key]) ? $build[$className][$key]:$defaultValue; + } + } + + $build[$className]['className'] = $className; + $build[$className]['tableName'] = $tableName; + $build[$className]['columns'] = $columns; + + // Make sure that anything else that is specified in the schema makes it to the final array + $build[$className] = IPF_ORM_Utils::arrayDeepMerge($table, $build[$className]); + + // We need to keep track of the className for the connection + $build[$className]['connectionClassName'] = $build[$className]['className']; + } + + return $build; + } + + protected function _processInheritance($array) + { + // Apply default inheritance configuration + foreach ($array as $className => $definition) { + if ( ! empty($array[$className]['inheritance'])) { + $this->_validateSchemaElement('inheritance', array_keys($definition['inheritance']), $className . '->inheritance'); + + // Default inheritance to concrete inheritance + if ( ! isset($array[$className]['inheritance']['type'])) { + $array[$className]['inheritance']['type'] = 'concrete'; + } + + // Some magic for setting up the keyField and keyValue column aggregation options + // Adds keyField to the parent class automatically + if ($array[$className]['inheritance']['type'] == 'column_aggregation') { + // Set the keyField to 'type' by default + if ( ! isset($array[$className]['inheritance']['keyField'])) { + $array[$className]['inheritance']['keyField'] = 'type'; + } + + // Set the keyValue to the name of the child class if it does not exist + if ( ! isset($array[$className]['inheritance']['keyValue'])) { + $array[$className]['inheritance']['keyValue'] = $className; + } + + // Add the keyType column to the parent if a definition does not already exist + if ( ! isset($array[$array[$className]['inheritance']['extends']]['columns'][$array[$className]['inheritance']['keyField']])) { + $array[$definition['inheritance']['extends']]['columns'][$array[$className]['inheritance']['keyField']] = array('name' => $array[$className]['inheritance']['keyField'], 'type' => 'string', 'length' => 255); + } + } + } + } + + // Array of the array keys to move to the parent, and the value to default the child definition to + // after moving it. Will also populate the subclasses array for the inheritance parent + $moves = array('columns' => array()); + + foreach ($array as $className => $definition) { + $parent = $this->_findBaseSuperClass($array, $definition['className']); + // Move any definitions on the schema to the parent + if (isset($definition['inheritance']['extends']) && isset($definition['inheritance']['type']) && ($definition['inheritance']['type'] == 'simple' || $definition['inheritance']['type'] == 'column_aggregation')) { + foreach ($moves as $move => $resetValue) { + $array[$parent][$move] = IPF_ORM_Utils::arrayDeepMerge($array[$parent][$move], $definition[$move]); + $array[$definition['className']][$move] = $resetValue; + } + + // Populate the parents subclasses + if ($definition['inheritance']['type'] == 'column_aggregation') { + $array[$parent]['inheritance']['subclasses'][$definition['className']] = array($definition['inheritance']['keyField'] => $definition['inheritance']['keyValue']); + } + } + } + + return $array; + } + + protected function _findBaseSuperClass($array, $class) + { + if (isset($array[$class]['inheritance']['extends'])) { + return $this->_findBaseSuperClass($array, $array[$class]['inheritance']['extends']); + } else { + return $class; + } + } + + protected function _buildRelationships($array) + { + // Handle auto detecting relations by the names of columns + // User.contact_id will automatically create User hasOne Contact local => contact_id, foreign => id + foreach ($array as $className => $properties) { + if (isset($properties['columns']) && ! empty($properties['columns']) && isset($properties['detect_relations']) && $properties['detect_relations']) { + foreach ($properties['columns'] as $column) { + // Check if the column we are inflecting has a _id on the end of it before trying to inflect it and find + // the class name for the column + if (strpos($column['name'], '_id')) { + $columnClassName = IPF_ORM_Inflector::classify(str_replace('_id', '', $column['name'])); + if (isset($array[$columnClassName]) && !isset($array[$className]['relations'][$columnClassName])) { + $array[$className]['relations'][$columnClassName] = array(); + + // Set the detected foreign key type and length to the same as the primary key + // of the related table + $type = isset($array[$columnClassName]['columns']['id']['type']) ? $array[$columnClassName]['columns']['id']['type']:'integer'; + $length = isset($array[$columnClassName]['columns']['id']['length']) ? $array[$columnClassName]['columns']['id']['length']:8; + $array[$className]['columns'][$column['name']]['type'] = $type; + $array[$className]['columns'][$column['name']]['length'] = $length; + } + } + } + } + } + + foreach ($array as $name => $properties) { + if ( ! isset($properties['relations'])) { + continue; + } + + $className = $properties['className']; + $relations = $properties['relations']; + + foreach ($relations as $alias => $relation) { + $class = isset($relation['class']) ? $relation['class']:$alias; + if ( ! isset($array[$class])) { + continue; + } + $relation['class'] = $class; + $relation['alias'] = isset($relation['alias']) ? $relation['alias'] : $alias; + + // Attempt to guess the local and foreign + if (isset($relation['refClass'])) { + $relation['local'] = isset($relation['local']) ? $relation['local']:IPF_ORM_Inflector::tableize($name) . '_id'; + $relation['foreign'] = isset($relation['foreign']) ? $relation['foreign']:IPF_ORM_Inflector::tableize($class) . '_id'; + } else { + $relation['local'] = isset($relation['local']) ? $relation['local']:IPF_ORM_Inflector::tableize($relation['class']) . '_id'; + $relation['foreign'] = isset($relation['foreign']) ? $relation['foreign']:'id'; + } + + if (isset($relation['refClass'])) { + $relation['type'] = 'many'; + } + + if (isset($relation['type']) && $relation['type']) { + $relation['type'] = $relation['type'] === 'one' ? IPF_ORM_Relation::ONE:IPF_ORM_Relation::MANY; + } else { + $relation['type'] = IPF_ORM_Relation::ONE; + } + + if (isset($relation['foreignType']) && $relation['foreignType']) { + $relation['foreignType'] = $relation['foreignType'] === 'one' ? IPF_ORM_Relation::ONE:IPF_ORM_Relation::MANY; + } + + $relation['key'] = $this->_buildUniqueRelationKey($relation); + + $this->_validateSchemaElement('relation', array_keys($relation), $className . '->relation->' . $relation['alias']); + + $this->_relations[$className][$alias] = $relation; + } + } + + // Now we auto-complete opposite ends of relationships + $this->_autoCompleteOppositeRelations(); + + // Make sure we do not have any duplicate relations + $this->_fixDuplicateRelations(); + + // Set the full array of relationships for each class to the final array + foreach ($this->_relations as $className => $relations) { + $array[$className]['relations'] = $relations; + } + + return $array; + } + + protected function _autoCompleteOppositeRelations() + { + foreach($this->_relations as $className => $relations) { + foreach ($relations AS $alias => $relation) { + if ((isset($relation['equal']) && $relation['equal']) || (isset($relation['autoComplete']) && $relation['autoComplete'] === false)) { + continue; + } + + $newRelation = array(); + $newRelation['foreign'] = $relation['local']; + $newRelation['local'] = $relation['foreign']; + $newRelation['class'] = isset($relation['foreignClass']) ? $relation['foreignClass']:$className; + $newRelation['alias'] = isset($relation['foreignAlias']) ? $relation['foreignAlias']:$className; + + // this is so that we know that this relation was autogenerated and + // that we do not need to include it if it is explicitly declared in the schema by the users. + $newRelation['autogenerated'] = true; + + if (isset($relation['refClass'])) { + $newRelation['refClass'] = $relation['refClass']; + $newRelation['type'] = isset($relation['foreignType']) ? $relation['foreignType']:$relation['type']; + } else { + if(isset($relation['foreignType'])) { + $newRelation['type'] = $relation['foreignType']; + } else { + $newRelation['type'] = $relation['type'] === IPF_ORM_Relation::ONE ? IPF_ORM_Relation::MANY:IPF_ORM_Relation::ONE; + } + } + + // Make sure it doesn't already exist + if ( ! isset($this->_relations[$relation['class']][$newRelation['alias']])) { + $newRelation['key'] = $this->_buildUniqueRelationKey($newRelation); + $this->_relations[$relation['class']][$newRelation['alias']] = $newRelation; + } + } + } + } + + protected function _fixDuplicateRelations() + { + foreach($this->_relations as $className => $relations) { + // This is for checking for duplicates between alias-relations and a auto-generated relations to ensure the result set of unique relations + $existingRelations = array(); + $uniqueRelations = array(); + foreach ($relations as $relation) { + if ( ! in_array($relation['key'], $existingRelations)) { + $existingRelations[] = $relation['key']; + $uniqueRelations = array_merge($uniqueRelations, array($relation['alias'] => $relation)); + } else { + // check to see if this relationship is not autogenerated, if it's not, then the user must have explicitly declared it + if ( ! isset($relation['autogenerated']) || $relation['autogenerated'] != true) { + $uniqueRelations = array_merge($uniqueRelations, array($relation['alias'] => $relation)); + } + } + } + + $this->_relations[$className] = $uniqueRelations; + } + } + + protected function _buildUniqueRelationKey($relation) + { + return md5($relation['local'].$relation['foreign'].$relation['class'].(isset($relation['refClass']) ? $relation['refClass']:null)); + } + + protected function _validateSchemaElement($name, $element, $path) + { + $element = (array) $element; + + $validation = $this->_validation[$name]; + + // Validators are a part of the column validation + // This should be fixed, made cleaner + if ($name == 'column') { + $validators = $this->getValidators(); + $validation = array_merge($validation, $validators); + } + + $validation = array_flip($validation); + + foreach ($element as $key => $value) { + if ( ! isset($validation[$value])) { + throw new IPF_ORM_Exception(sprintf('Invalid schema element named "' . $value . '" + at path "' . $path . '"')); + } + } + } +} \ No newline at end of file diff --git a/ipf/orm/inflector.php b/ipf/orm/inflector.php new file mode 100644 index 0000000..6a6561b --- /dev/null +++ b/ipf/orm/inflector.php @@ -0,0 +1,315 @@ + '\1zes', + '/^(ox)$/i' => '\1en', + '/([m|l])ouse$/i' => '\1ice', + '/(matr|vert|ind)ix|ex$/i' => '\1ices', + '/(x|ch|ss|sh)$/i' => '\1es', + '/([^aeiouy]|qu)ies$/i' => '\1y', + '/([^aeiouy]|qu)y$/i' => '\1ies', + '/(hive)$/i' => '\1s', + '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves', + '/sis$/i' => 'ses', + '/([ti])um$/i' => '\1a', + '/(buffal|tomat)o$/i' => '\1oes', + '/(bu)s$/i' => '\1ses', + '/(alias|status)/i' => '\1es', + '/(octop|vir)us$/i' => '\1i', + '/(ax|test)is$/i' => '\1es', + '/s$/i' => 's', + '/$/' => 's'); + + $uncountable = array('equipment', + 'information', + 'rice', + 'money', + 'species', + 'series', + 'fish', + 'sheep'); + + $irregular = array('person' => 'people', + 'man' => 'men', + 'child' => 'children', + 'sex' => 'sexes', + 'move' => 'moves'); + + $lowercasedWord = strtolower($word); + + foreach ($uncountable as $_uncountable) { + if(substr($lowercasedWord, (-1 * strlen($_uncountable))) == $_uncountable) { + return $word; + } + } + + foreach ($irregular as $_plural=> $_singular){ + if (preg_match('/('.$_plural.')$/i', $word, $arr)) { + return preg_replace('/('.$_plural.')$/i', substr($arr[0],0,1) . substr($_singular,1), $word); + } + } + + foreach ($plural as $rule => $replacement) { + if (preg_match($rule, $word)) { + return preg_replace($rule, $replacement, $word); + } + } + + return false; + } + + public static function singularize($word) + { + $singular = array('/(quiz)zes$/i' => '\\1', + '/(matr)ices$/i' => '\\1ix', + '/(vert|ind)ices$/i' => '\\1ex', + '/^(ox)en/i' => '\\1', + '/(alias|status)es$/i' => '\\1', + '/([octop|vir])i$/i' => '\\1us', + '/(cris|ax|test)es$/i' => '\\1is', + '/(shoe)s$/i' => '\\1', + '/(o)es$/i' => '\\1', + '/(bus)es$/i' => '\\1', + '/([m|l])ice$/i' => '\\1ouse', + '/(x|ch|ss|sh)es$/i' => '\\1', + '/(m)ovies$/i' => '\\1ovie', + '/(s)eries$/i' => '\\1eries', + '/([^aeiouy]|qu)ies$/i' => '\\1y', + '/([lr])ves$/i' => '\\1f', + '/(tive)s$/i' => '\\1', + '/(hive)s$/i' => '\\1', + '/([^f])ves$/i' => '\\1fe', + '/(^analy)ses$/i' => '\\1sis', + '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\\1\\2sis', + '/([ti])a$/i' => '\\1um', + '/(n)ews$/i' => '\\1ews', + '/^(.{2,2})$/i' => '\\1', + '/s$/i' => ''); + + $uncountable = array('equipment', + 'information', + 'rice', + 'money', + 'species', + 'series', + 'fish', + 'sheep', + 'sms', + 'status', + 'access'); + + $irregular = array('person' => 'people', + 'man' => 'men', + 'child' => 'children', + 'sex' => 'sexes', + 'move' => 'moves'); + + $lowercasedWord = strtolower($word); + foreach ($uncountable as $_uncountable){ + if(substr($lowercasedWord, ( -1 * strlen($_uncountable))) == $_uncountable){ + return $word; + } + } + + foreach ($irregular as $_singular => $_plural) { + if (preg_match('/('.$_plural.')$/i', $word, $arr)) { + return preg_replace('/('.$_plural.')$/i', substr($arr[0],0,1).substr($_singular,1), $word); + } + } + + foreach ($singular as $rule => $replacement) { + if (preg_match($rule, $word)) { + return preg_replace($rule, $replacement, $word); + } + } + + return $word; + } + + public static function tableize($word) + { + // Would prefer this but it breaks unit tests. Forces the table underscore pattern + // return self::pluralize(self::underscore($name)); + return strtolower(preg_replace('~(?<=\\w)([A-Z])~', '_$1', $word)); + } + + public static function classify($word) + { + return preg_replace_callback('~(_?)(_)([\w])~', array("IPF_ORM_Inflector", "classifyCallback"), ucfirst(strtolower($word))); + } + + public static function classifyCallback($matches) + { + return $matches[1] . strtoupper($matches[3]); + } + + public static function seemsUtf8($string) + { + for ($i = 0; $i < strlen($string); $i++) { + if (ord($string[$i]) < 0x80) continue; # 0bbbbbbb + elseif ((ord($string[$i]) & 0xE0) == 0xC0) $n=1; # 110bbbbb + elseif ((ord($string[$i]) & 0xF0) == 0xE0) $n=2; # 1110bbbb + elseif ((ord($string[$i]) & 0xF8) == 0xF0) $n=3; # 11110bbb + elseif ((ord($string[$i]) & 0xFC) == 0xF8) $n=4; # 111110bb + elseif ((ord($string[$i]) & 0xFE) == 0xFC) $n=5; # 1111110b + else return false; # Does not match any model + for ($j=0; $j<$n; $j++) { # n bytes matching 10bbbbbb follow ? + if ((++$i == strlen($string)) || ((ord($string[$i]) & 0xC0) != 0x80)) + return false; + } + } + return true; + } + + public static function unaccent($string) + { + if ( ! preg_match('/[\x80-\xff]/', $string) ) { + return $string; + } + + if (self::seemsUtf8($string)) { + $chars = array( + // Decompositions for Latin-1 Supplement + chr(195).chr(128) => 'A', chr(195).chr(129) => 'A', + chr(195).chr(130) => 'A', chr(195).chr(131) => 'A', + chr(195).chr(132) => 'A', chr(195).chr(133) => 'A', + chr(195).chr(135) => 'C', chr(195).chr(136) => 'E', + chr(195).chr(137) => 'E', chr(195).chr(138) => 'E', + chr(195).chr(139) => 'E', chr(195).chr(140) => 'I', + chr(195).chr(141) => 'I', chr(195).chr(142) => 'I', + chr(195).chr(143) => 'I', chr(195).chr(145) => 'N', + chr(195).chr(146) => 'O', chr(195).chr(147) => 'O', + chr(195).chr(148) => 'O', chr(195).chr(149) => 'O', + chr(195).chr(150) => 'O', chr(195).chr(153) => 'U', + chr(195).chr(154) => 'U', chr(195).chr(155) => 'U', + chr(195).chr(156) => 'U', chr(195).chr(157) => 'Y', + chr(195).chr(159) => 's', chr(195).chr(160) => 'a', + chr(195).chr(161) => 'a', chr(195).chr(162) => 'a', + chr(195).chr(163) => 'a', chr(195).chr(164) => 'a', + chr(195).chr(165) => 'a', chr(195).chr(167) => 'c', + chr(195).chr(168) => 'e', chr(195).chr(169) => 'e', + chr(195).chr(170) => 'e', chr(195).chr(171) => 'e', + chr(195).chr(172) => 'i', chr(195).chr(173) => 'i', + chr(195).chr(174) => 'i', chr(195).chr(175) => 'i', + chr(195).chr(177) => 'n', chr(195).chr(178) => 'o', + chr(195).chr(179) => 'o', chr(195).chr(180) => 'o', + chr(195).chr(181) => 'o', chr(195).chr(182) => 'o', + chr(195).chr(182) => 'o', chr(195).chr(185) => 'u', + chr(195).chr(186) => 'u', chr(195).chr(187) => 'u', + chr(195).chr(188) => 'u', chr(195).chr(189) => 'y', + chr(195).chr(191) => 'y', + // Decompositions for Latin Extended-A + chr(196).chr(128) => 'A', chr(196).chr(129) => 'a', + chr(196).chr(130) => 'A', chr(196).chr(131) => 'a', + chr(196).chr(132) => 'A', chr(196).chr(133) => 'a', + chr(196).chr(134) => 'C', chr(196).chr(135) => 'c', + chr(196).chr(136) => 'C', chr(196).chr(137) => 'c', + chr(196).chr(138) => 'C', chr(196).chr(139) => 'c', + chr(196).chr(140) => 'C', chr(196).chr(141) => 'c', + chr(196).chr(142) => 'D', chr(196).chr(143) => 'd', + chr(196).chr(144) => 'D', chr(196).chr(145) => 'd', + chr(196).chr(146) => 'E', chr(196).chr(147) => 'e', + chr(196).chr(148) => 'E', chr(196).chr(149) => 'e', + chr(196).chr(150) => 'E', chr(196).chr(151) => 'e', + chr(196).chr(152) => 'E', chr(196).chr(153) => 'e', + chr(196).chr(154) => 'E', chr(196).chr(155) => 'e', + chr(196).chr(156) => 'G', chr(196).chr(157) => 'g', + chr(196).chr(158) => 'G', chr(196).chr(159) => 'g', + chr(196).chr(160) => 'G', chr(196).chr(161) => 'g', + chr(196).chr(162) => 'G', chr(196).chr(163) => 'g', + chr(196).chr(164) => 'H', chr(196).chr(165) => 'h', + chr(196).chr(166) => 'H', chr(196).chr(167) => 'h', + chr(196).chr(168) => 'I', chr(196).chr(169) => 'i', + chr(196).chr(170) => 'I', chr(196).chr(171) => 'i', + chr(196).chr(172) => 'I', chr(196).chr(173) => 'i', + chr(196).chr(174) => 'I', chr(196).chr(175) => 'i', + chr(196).chr(176) => 'I', chr(196).chr(177) => 'i', + chr(196).chr(178) => 'IJ',chr(196).chr(179) => 'ij', + chr(196).chr(180) => 'J', chr(196).chr(181) => 'j', + chr(196).chr(182) => 'K', chr(196).chr(183) => 'k', + chr(196).chr(184) => 'k', chr(196).chr(185) => 'L', + chr(196).chr(186) => 'l', chr(196).chr(187) => 'L', + chr(196).chr(188) => 'l', chr(196).chr(189) => 'L', + chr(196).chr(190) => 'l', chr(196).chr(191) => 'L', + chr(197).chr(128) => 'l', chr(197).chr(129) => 'L', + chr(197).chr(130) => 'l', chr(197).chr(131) => 'N', + chr(197).chr(132) => 'n', chr(197).chr(133) => 'N', + chr(197).chr(134) => 'n', chr(197).chr(135) => 'N', + chr(197).chr(136) => 'n', chr(197).chr(137) => 'N', + chr(197).chr(138) => 'n', chr(197).chr(139) => 'N', + chr(197).chr(140) => 'O', chr(197).chr(141) => 'o', + chr(197).chr(142) => 'O', chr(197).chr(143) => 'o', + chr(197).chr(144) => 'O', chr(197).chr(145) => 'o', + chr(197).chr(146) => 'OE',chr(197).chr(147) => 'oe', + chr(197).chr(148) => 'R', chr(197).chr(149) => 'r', + chr(197).chr(150) => 'R', chr(197).chr(151) => 'r', + chr(197).chr(152) => 'R', chr(197).chr(153) => 'r', + chr(197).chr(154) => 'S', chr(197).chr(155) => 's', + chr(197).chr(156) => 'S', chr(197).chr(157) => 's', + chr(197).chr(158) => 'S', chr(197).chr(159) => 's', + chr(197).chr(160) => 'S', chr(197).chr(161) => 's', + chr(197).chr(162) => 'T', chr(197).chr(163) => 't', + chr(197).chr(164) => 'T', chr(197).chr(165) => 't', + chr(197).chr(166) => 'T', chr(197).chr(167) => 't', + chr(197).chr(168) => 'U', chr(197).chr(169) => 'u', + chr(197).chr(170) => 'U', chr(197).chr(171) => 'u', + chr(197).chr(172) => 'U', chr(197).chr(173) => 'u', + chr(197).chr(174) => 'U', chr(197).chr(175) => 'u', + chr(197).chr(176) => 'U', chr(197).chr(177) => 'u', + chr(197).chr(178) => 'U', chr(197).chr(179) => 'u', + chr(197).chr(180) => 'W', chr(197).chr(181) => 'w', + chr(197).chr(182) => 'Y', chr(197).chr(183) => 'y', + chr(197).chr(184) => 'Y', chr(197).chr(185) => 'Z', + chr(197).chr(186) => 'z', chr(197).chr(187) => 'Z', + chr(197).chr(188) => 'z', chr(197).chr(189) => 'Z', + chr(197).chr(190) => 'z', chr(197).chr(191) => 's', + // Euro Sign + chr(226).chr(130).chr(172) => 'E', + // GBP (Pound) Sign + chr(194).chr(163) => ''); + + $string = strtr($string, $chars); + } else { + // Assume ISO-8859-1 if not UTF-8 + $chars['in'] = chr(128).chr(131).chr(138).chr(142).chr(154).chr(158) + .chr(159).chr(162).chr(165).chr(181).chr(192).chr(193).chr(194) + .chr(195).chr(196).chr(197).chr(199).chr(200).chr(201).chr(202) + .chr(203).chr(204).chr(205).chr(206).chr(207).chr(209).chr(210) + .chr(211).chr(212).chr(213).chr(214).chr(216).chr(217).chr(218) + .chr(219).chr(220).chr(221).chr(224).chr(225).chr(226).chr(227) + .chr(228).chr(229).chr(231).chr(232).chr(233).chr(234).chr(235) + .chr(236).chr(237).chr(238).chr(239).chr(241).chr(242).chr(243) + .chr(244).chr(245).chr(246).chr(248).chr(249).chr(250).chr(251) + .chr(252).chr(253).chr(255); + + $chars['out'] = "EfSZszYcYuAAAAAACEEEEIIIINOOOOOOUUUUYaaaaaaceeeeiiiinoooooouuuuyy"; + + $string = strtr($string, $chars['in'], $chars['out']); + $doubleChars['in'] = array(chr(140), chr(156), chr(198), chr(208), chr(222), chr(223), chr(230), chr(240), chr(254)); + $doubleChars['out'] = array('OE', 'oe', 'AE', 'DH', 'TH', 'ss', 'ae', 'dh', 'th'); + $string = str_replace($doubleChars['in'], $doubleChars['out'], $string); + } + + return $string; + } + + public static function urlize($text) + { + // Remove all non url friendly characters with the unaccent function + $text = self::unaccent($text); + + // Remove all none word characters + $text = preg_replace('/\W/', ' ', $text); + + // More stripping. Replace spaces with dashes + $text = strtolower(preg_replace('/[^A-Z^a-z^0-9^\/]+/', '-', + preg_replace('/([a-z\d])([A-Z])/', '\1_\2', + preg_replace('/([A-Z]+)([A-Z][a-z])/', '\1_\2', + preg_replace('/::/', '/', $text))))); + + return trim($text, '-'); + } +} \ No newline at end of file diff --git a/ipf/orm/locator.php b/ipf/orm/locator.php new file mode 100644 index 0000000..aaf4998 --- /dev/null +++ b/ipf/orm/locator.php @@ -0,0 +1,93 @@ + $resource) { + if ($resource instanceof IPF_ORM_Locator_Injectable) { + $resource->setLocator($this); + } + $this->_resources[$name] = $resource; + } + } + self::$_instances[] = $this; + } + + public static function instance() + { + if (empty(self::$_instances)) { + $obj = new IPF_ORM_Locator(); + } + return current(self::$_instances); + } + + public function setClassPrefix($prefix) + { + $this->_classPrefix = $prefix; + } + + public function getClassPrefix() + { + return $this->_classPrefix; + } + + public function contains($name) + { + return isset($this->_resources[$name]); + } + + public function bind($name, $value) + { + $this->_resources[$name] = $value; + + return $this; + } + + public function locate($name) + { + if (isset($this->_resources[$name])) { + return $this->_resources[$name]; + } else { + $className = $name; + + if ( ! class_exists($className)) { + + $name = explode('.', $name); + $name = array_map('strtolower', $name); + $name = array_map('ucfirst', $name); + $name = implode('_', $name); + + $className = $this->_classPrefix . $name; + + if ( ! class_exists($className)) { + throw new IPF_ORM_Exception_Locator("Couldn't locate resource " . $className); + } + } + + $this->_resources[$name] = new $className(); + + if ($this->_resources[$name] instanceof IPF_ORM_Locator_Injectable) { + $this->_resources[$name]->setLocator($this); + } + + return $this->_resources[$name]; + } + + throw new IPF_ORM_Exception_Locator("Couldn't locate resource " . $name); + } + + public function count() + { + return count($this->_resources); + } + + public function getIterator() + { + return new ArrayIterator($this->_resources); + } +} diff --git a/ipf/orm/locator/injectable.php b/ipf/orm/locator/injectable.php new file mode 100644 index 0000000..69460d9 --- /dev/null +++ b/ipf/orm/locator/injectable.php @@ -0,0 +1,57 @@ +_locator = $locator; + return $this; + } + + public function getLocator() + { + if ( ! isset($this->_locator)) { + $this->_locator = IPF_ORM_Locator::instance(); + + } + return $this->_locator; + } + + public function locate($name) + { + if (isset($this->_resources[$name])) { + if (is_object($this->_resources[$name])) { + return $this->_resources[$name]; + } else { + // get the name of the concrete implementation + $concreteImpl = $this->_resources[$name]; + + return $this->getLocator()->locate($concreteImpl); + } + } else { + return $this->getLocator()->locate($name); + } + } + + public function bind($name, $resource) + { + $this->_resources[$name] = $resource; + + return $this; + } + + public static function initNullObject(IPF_ORM_Null $null) + { + self::$_null = $null; + } + + public static function getNullObject() + { + return self::$_null; + } +} \ No newline at end of file diff --git a/ipf/orm/manager.php b/ipf/orm/manager.php new file mode 100644 index 0000000..3b8afd9 --- /dev/null +++ b/ipf/orm/manager.php @@ -0,0 +1,430 @@ + null, + IPF_ORM::ATTR_RESULT_CACHE => null, + IPF_ORM::ATTR_QUERY_CACHE => null, + IPF_ORM::ATTR_LOAD_REFERENCES => true, + IPF_ORM::ATTR_LISTENER => new IPF_ORM_EventListener(), + IPF_ORM::ATTR_RECORD_LISTENER => new IPF_ORM_Record_Listener(), + IPF_ORM::ATTR_THROW_EXCEPTIONS => true, + IPF_ORM::ATTR_VALIDATE => IPF_ORM::VALIDATE_NONE, + IPF_ORM::ATTR_QUERY_LIMIT => IPF_ORM::LIMIT_RECORDS, + IPF_ORM::ATTR_IDXNAME_FORMAT => "%s_idx", + IPF_ORM::ATTR_SEQNAME_FORMAT => "%s_seq", + IPF_ORM::ATTR_TBLNAME_FORMAT => "%s", + IPF_ORM::ATTR_QUOTE_IDENTIFIER => false, + IPF_ORM::ATTR_SEQCOL_NAME => 'id', + IPF_ORM::ATTR_PORTABILITY => IPF_ORM::PORTABILITY_ALL, + IPF_ORM::ATTR_EXPORT => IPF_ORM::EXPORT_ALL, + IPF_ORM::ATTR_DECIMAL_PLACES => 2, + IPF_ORM::ATTR_DEFAULT_PARAM_NAMESPACE => 'ipf', + IPF_ORM::ATTR_AUTOLOAD_TABLE_CLASSES => false, + IPF_ORM::ATTR_USE_DQL_CALLBACKS => false, + ); + foreach ($attributes as $attribute => $value) { + $old = $this->getAttribute($attribute); + if ($old === null) { + $this->setAttribute($attribute,$value); + } + } + return true; + } + return false; + } + + public static function getInstance() + { + static $instance; + if ( ! isset($instance)) { + $instance = new self(); + } + return $instance; + } + + public function getQueryRegistry() + { + if ( ! isset($this->_queryRegistry)) { + $this->_queryRegistry = new IPF_ORM_Query_Registry; + } + return $this->_queryRegistry; + } + + public function setQueryRegistry(IPF_ORM_Query_Registry $registry) + { + $this->_queryRegistry = $registry; + + return $this; + } + + public static function connection($adapter = null, $name = null) + { + if ($adapter == null) { + return IPF_ORM_Manager::getInstance()->getCurrentConnection(); + } else { + return IPF_ORM_Manager::getInstance()->openConnection($adapter, $name); + } + } + + public function openConnection($adapter, $name = null, $setCurrent = true) + { + if (is_object($adapter)) { + if ( ! ($adapter instanceof PDO) && ! in_array('IPF_ORM_Adapter_Interface', class_implements($adapter))) { + throw new IPF_ORM_Exception("First argument should be an instance of PDO or implement IPF_ORM_Adapter_Interface"); + } + + $driverName = $adapter->getAttribute(IPF_ORM::ATTR_DRIVER_NAME); + } else if (is_array($adapter)) { + if ( ! isset($adapter[0])) { + throw new IPF_ORM_Exception('Empty data source name given.'); + } + $e = explode(':', $adapter[0]); + + if ($e[0] == 'uri') { + $e[0] = 'odbc'; + } + + $parts['dsn'] = $adapter[0]; + $parts['scheme'] = $e[0]; + $parts['user'] = (isset($adapter[1])) ? $adapter[1] : null; + $parts['pass'] = (isset($adapter[2])) ? $adapter[2] : null; + $driverName = $e[0]; + $adapter = $parts; + } else { + $parts = $this->parseDsn($adapter); + $driverName = $parts['scheme']; + $adapter = $parts; + } + + // Decode adapter information + if (is_array($adapter)) { + foreach ($adapter as $key => $value) { + $adapter[$key] = $value?urldecode($value):null; + } + } + + // initialize the default attributes + $this->setDefaultAttributes(); + + if ($name !== null) { + $name = (string) $name; + if (isset($this->_connections[$name])) { + if ($setCurrent) { + $this->_currIndex = $name; + } + return $this->_connections[$name]; + } + } else { + $name = $this->_index; + $this->_index++; + } + + $drivers = array('mysql' => 'IPF_ORM_Connection_Mysql', + //'sqlite' => 'IPF_ORM_Connection_Sqlite', + //'pgsql' => 'IPF_ORM_Connection_Pgsql', + //'oci' => 'IPF_ORM_Connection_Oracle', + //'oci8' => 'IPF_ORM_Connection_Oracle', + //'oracle' => 'IPF_ORM_Connection_Oracle', + //'mssql' => 'IPF_ORM_Connection_Mssql', + //'dblib' => 'IPF_ORM_Connection_Mssql', + //'firebird' => 'IPF_ORM_Connection_Firebird', + //'informix' => 'IPF_ORM_Connection_Informix', + //'mock' => 'IPF_ORM_Connection_Mock' + ); + + if ( ! isset($drivers[$driverName])) { + throw new IPF_ORM_Exception('Unknown driver ' . $driverName); + } + + $className = $drivers[$driverName]; + $conn = new $className($this, $adapter); + $conn->setName($name); + + $this->_connections[$name] = $conn; + + if ($setCurrent) { + $this->_currIndex = $name; + } + return $this->_connections[$name]; + } + + public function parsePdoDsn($dsn) + { + $parts = array(); + + $names = array('dsn', 'scheme', 'host', 'port', 'user', 'pass', 'path', 'query', 'fragment', 'unix_socket'); + + foreach ($names as $name) { + if ( ! isset($parts[$name])) { + $parts[$name] = null; + } + } + + $e = explode(':', $dsn); + $parts['scheme'] = $e[0]; + $parts['dsn'] = $dsn; + + $e = explode(';', $e[1]); + foreach ($e as $string) { + if ($string) { + $e2 = explode('=', $string); + + if (isset($e2[0]) && isset($e2[1])) { + list($key, $value) = $e2; + $parts[$key] = $value; + } + } + } + + return $parts; + } + + protected function _buildDsnPartsArray($dsn) + { + // fix sqlite dsn so that it will parse correctly + $dsn = str_replace("////", "/", $dsn); + $dsn = str_replace("\\", "/", $dsn); + $dsn = preg_replace("/\/\/\/(.*):\//", "//$1:/", $dsn); + + // silence any warnings + $parts = @parse_url($dsn); + + $names = array('dsn', 'scheme', 'host', 'port', 'user', 'pass', 'path', 'query', 'fragment', 'unix_socket'); + + foreach ($names as $name) { + if ( ! isset($parts[$name])) { + $parts[$name] = null; + } + } + + if (count($parts) == 0 || ! isset($parts['scheme'])) { + throw new IPF_ORM_Exception('Could not parse dsn'); + } + + return $parts; + } + + public function parseDsn($dsn) + { + $parts = $this->_buildDsnPartsArray($dsn); + + switch ($parts['scheme']) { + case 'sqlite': + case 'sqlite2': + case 'sqlite3': + if (isset($parts['host']) && $parts['host'] == ':memory') { + $parts['database'] = ':memory:'; + $parts['dsn'] = 'sqlite::memory:'; + } else { + //fix windows dsn we have to add host: to path and set host to null + if (isset($parts['host'])) { + $parts['path'] = $parts['host'] . ":" . $parts["path"]; + $parts['host'] = null; + } + $parts['database'] = $parts['path']; + $parts['dsn'] = $parts['scheme'] . ':' . $parts['path']; + } + + break; + + case 'mssql': + case 'dblib': + if ( ! isset($parts['path']) || $parts['path'] == '/') { + throw new IPF_ORM_Exception('No database available in data source name'); + } + if (isset($parts['path'])) { + $parts['database'] = substr($parts['path'], 1); + } + if ( ! isset($parts['host'])) { + throw new IPF_ORM_Exception('No hostname set in data source name'); + } + + $parts['dsn'] = $parts['scheme'] . ':host=' + . $parts['host'] . (isset($parts['port']) ? ':' . $parts['port']:null) . ';dbname=' + . $parts['database']; + + break; + + case 'mysql': + case 'informix': + case 'oci8': + case 'oci': + case 'firebird': + case 'pgsql': + case 'odbc': + case 'mock': + case 'oracle': + if ( ! isset($parts['path']) || $parts['path'] == '/') { + throw new IPF_ORM_Exception('No database available in data source name'); + } + if (isset($parts['path'])) { + $parts['database'] = substr($parts['path'], 1); + } + if ( ! isset($parts['host'])) { + throw new IPF_ORM_Exception('No hostname set in data source name'); + } + + $parts['dsn'] = $parts['scheme'] . ':host=' + . $parts['host'] . (isset($parts['port']) ? ';port=' . $parts['port']:null) . ';dbname=' + . $parts['database']; + + break; + default: + throw new IPF_ORM_Exception('Unknown driver '.$parts['scheme']); + } + + return $parts; + } + + public function getConnection($name) + { + if ( ! isset($this->_connections[$name])) { + throw new IPF_ORM_Exception('Unknown connection: ' . $name); + } + + return $this->_connections[$name]; + } + + public function getConnectionName(IPF_ORM_Connection $conn) + { + return array_search($conn, $this->_connections, true); + } + + public function bindComponent($componentName, $connectionName) + { + $this->_bound[$componentName] = $connectionName; + } + + public function getConnectionForComponent($componentName) + { + //IPF_ORM::autoload($componentName); + + if (isset($this->_bound[$componentName])) { + return $this->getConnection($this->_bound[$componentName]); + } + + return $this->getCurrentConnection(); + } + + public function hasConnectionForComponent($componentName = null) + { + return isset($this->_bound[$componentName]); + } + + public function closeConnection(IPF_ORM_Connection $connection) + { + $connection->close(); + + $key = array_search($connection, $this->_connections, true); + + if ($key !== false) { + unset($this->_connections[$key]); + } + $this->_currIndex = key($this->_connections); + + unset($connection); + } + + public function getConnections() + { + return $this->_connections; + } + + public function setCurrentConnection($key) + { + $key = (string) $key; + if ( ! isset($this->_connections[$key])) { + throw new InvalidKeyException(); + } + $this->_currIndex = $key; + } + + public function contains($key) + { + return isset($this->_connections[$key]); + } + + public function count() + { + return count($this->_connections); + } + + public function getIterator() + { + return new ArrayIterator($this->_connections); + } + + public function getCurrentConnection() + { + $i = $this->_currIndex; + if ( ! isset($this->_connections[$i])) { + throw new IPF_ORM_Exception('There is no open connection'); + } + return $this->_connections[$i]; + } + + public function createDatabases($specifiedConnections = array()) + { + if ( ! is_array($specifiedConnections)) { + $specifiedConnections = (array) $specifiedConnections; + } + + $results = array(); + + foreach ($this as $name => $connection) { + if ( ! empty($specifiedConnections) && ! in_array($name, $specifiedConnections)) { + continue; + } + + $results[$name] = $connection->createDatabase(); + } + + return $results; + } + + public function dropDatabases($specifiedConnections = array()) + { + if ( ! is_array($specifiedConnections)) { + $specifiedConnections = (array) $specifiedConnections; + } + + $results = array(); + + foreach ($this as $name => $connection) { + if ( ! empty($specifiedConnections) && ! in_array($name, $specifiedConnections)) { + continue; + } + + $results[$name] = $connection->dropDatabase(); + } + + return $results; + } + + public function __toString() + { + $r[] = "
    ";
    +        $r[] = "IPF_ORM_Manager";
    +        $r[] = "Connections : ".count($this->_connections);
    +        $r[] = "
    "; + return implode("\n",$r); + } +} diff --git a/ipf/orm/null.php b/ipf/orm/null.php new file mode 100644 index 0000000..20e813d --- /dev/null +++ b/ipf/orm/null.php @@ -0,0 +1,13 @@ +loadData($path); + } + + static public function dump($array, $type = 'xml', $path = null) + { + $parser = self::getParser($type); + return $parser->dumpData($array, $path); + } + + public function doLoad($path) + { + ob_start(); + if ( ! file_exists($path)) { + $contents = $path; + $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'dparser_' . microtime(); + + file_put_contents($path, $contents); + } + include($path); + $contents = iconv("UTF-8", "UTF-8", ob_get_clean()); + return $contents; + } + + public function doDump($data, $path = null) + { + if ($path !== null) { + return file_put_contents($path, $data); + } else { + return $data; + } + } +} \ No newline at end of file diff --git a/ipf/orm/parser/spyc.php b/ipf/orm/parser/spyc.php new file mode 100644 index 0000000..953d2b4 --- /dev/null +++ b/ipf/orm/parser/spyc.php @@ -0,0 +1,637 @@ +id = $nodeId; + } +} + +class IPF_ORM_Parser_Spyc { + + function YAMLLoad($input) { + $spyc = new IPF_ORM_Spyc; + return $spyc->load($input); + } + + function YAMLDump($array,$indent = false,$wordwrap = false) { + $spyc = new IPF_ORM_Spyc; + return $spyc->dump($array,$indent,$wordwrap); + } + + function load($input) { + if ( ! empty($input) && (strpos($input, "\n") === false) + && file_exists($input)) { + $yaml = file($input); + } else { + $yaml = explode("\n",$input); + } + $base = new IPF_ORM_Parser_Spyc_YamlNode (1); + $base->indent = 0; + $this->_lastIndent = 0; + $this->_lastNode = $base->id; + $this->_inBlock = false; + $this->_isInline = false; + $this->_nodeId = 2; + + foreach ($yaml as $linenum => $line) { + $ifchk = trim($line); + if (preg_match('/^(\t)+(\w+)/', $line)) { + $err = 'ERROR: Line '. ($linenum + 1) .' in your input YAML begins'. + ' with a tab. YAML only recognizes spaces. Please reformat.'; + die($err); + } + + if ($this->_inBlock === false && empty($ifchk)) { + continue; + } elseif ($this->_inBlock == true && empty($ifchk)) { + $last =& $this->_allNodes[$this->_lastNode]; + $last->data[key($last->data)] .= "\n"; + } elseif ($ifchk{0} != '#' && substr($ifchk,0,3) != '---') { + $node = new IPF_ORM_Parser_Spyc_YamlNode ($this->_nodeId); + $this->_nodeId++; + + $node->indent = $this->_getIndent($line); + + // Check where the node lies in the hierarchy + if ($this->_lastIndent == $node->indent) { + // If we're in a block, add the text to the parent's data + if ($this->_inBlock === true) { + $parent =& $this->_allNodes[$this->_lastNode]; + $parent->data[key($parent->data)] .= trim($line).$this->_blockEnd; + } else { + // The current node's parent is the same as the previous node's + if (isset($this->_allNodes[$this->_lastNode])) { + $node->parent = $this->_allNodes[$this->_lastNode]->parent; + } + } + } elseif ($this->_lastIndent < $node->indent) { + if ($this->_inBlock === true) { + $parent =& $this->_allNodes[$this->_lastNode]; + $parent->data[key($parent->data)] .= trim($line).$this->_blockEnd; + } elseif ($this->_inBlock === false) { + // The current node's parent is the previous node + $node->parent = $this->_lastNode; + + // If the value of the last node's data was > or | we need to + // start blocking i.e. taking in all lines as a text value until + // we drop our indent. + $parent =& $this->_allNodes[$node->parent]; + $this->_allNodes[$node->parent]->children = true; + if (is_array($parent->data)) { + $chk = ''; + if (isset ($parent->data[key($parent->data)])) + $chk = $parent->data[key($parent->data)]; + if ($chk === '>') { + $this->_inBlock = true; + $this->_blockEnd = ' '; + $parent->data[key($parent->data)] = + str_replace('>','',$parent->data[key($parent->data)]); + $parent->data[key($parent->data)] .= trim($line).' '; + $this->_allNodes[$node->parent]->children = false; + $this->_lastIndent = $node->indent; + } elseif ($chk === '|') { + $this->_inBlock = true; + $this->_blockEnd = "\n"; + $parent->data[key($parent->data)] = + str_replace('|','',$parent->data[key($parent->data)]); + $parent->data[key($parent->data)] .= trim($line)."\n"; + $this->_allNodes[$node->parent]->children = false; + $this->_lastIndent = $node->indent; + } + } + } + } elseif ($this->_lastIndent > $node->indent) { + // Any block we had going is dead now + if ($this->_inBlock === true) { + $this->_inBlock = false; + if ($this->_blockEnd = "\n") { + $last =& $this->_allNodes[$this->_lastNode]; + $last->data[key($last->data)] = + trim($last->data[key($last->data)]); + } + } + + // We don't know the parent of the node so we have to find it + // foreach ($this->_allNodes as $n) { + foreach ($this->_indentSort[$node->indent] as $n) { + if ($n->indent == $node->indent) { + $node->parent = $n->parent; + } + } + } + + if ($this->_inBlock === false) { + // Set these properties with information from our current node + $this->_lastIndent = $node->indent; + // Set the last node + $this->_lastNode = $node->id; + // Parse the YAML line and return its data + $node->data = $this->_parseLine($line); + // Add the node to the master list + $this->_allNodes[$node->id] = $node; + // Add a reference to the parent list + $this->_allParent[intval($node->parent)][] = $node->id; + // Add a reference to the node in an indent array + $this->_indentSort[$node->indent][] =& $this->_allNodes[$node->id]; + // Add a reference to the node in a References array if this node + // has a YAML reference in it. + if ( + ( (is_array($node->data)) && + isset($node->data[key($node->data)]) && + ( ! is_array($node->data[key($node->data)])) ) + && + ( (preg_match('/^&([^ ]+)/',$node->data[key($node->data)])) + || + (preg_match('/^\*([^ ]+)/',$node->data[key($node->data)])) ) + ) { + $this->_haveRefs[] =& $this->_allNodes[$node->id]; + } elseif ( + ( (is_array($node->data)) && + isset($node->data[key($node->data)]) && + (is_array($node->data[key($node->data)])) ) + ) { + // Incomplete reference making code. Ugly, needs cleaned up. + foreach ($node->data[key($node->data)] as $d) { + if ( !is_array($d) && + ( (preg_match('/^&([^ ]+)/',$d)) + || + (preg_match('/^\*([^ ]+)/',$d)) ) + ) { + $this->_haveRefs[] =& $this->_allNodes[$node->id]; + } + } + } + } + } + } + unset($node); + + // Here we travel through node-space and pick out references (& and *) + $this->_linkReferences(); + + // Build the PHP array out of node-space + $trunk = $this->_buildArray(); + return $trunk; + } + + function dump($array,$indent = false,$wordwrap = false) { + // Dumps to some very clean YAML. We'll have to add some more features + // and options soon. And better support for folding. + + // New features and options. + if ($indent === false or !is_numeric($indent)) { + $this->_dumpIndent = 2; + } else { + $this->_dumpIndent = $indent; + } + + if ($wordwrap === false or !is_numeric($wordwrap)) { + $this->_dumpWordWrap = 40; + } else { + $this->_dumpWordWrap = $wordwrap; + } + + // New YAML document + $string = "---\n"; + + // Start at the base of the array and move through it. + foreach ($array as $key => $value) { + $string .= $this->_yamlize($key,$value,0); + } + return $string; + } + + var $_haveRefs; + var $_allNodes; + var $_allParent; + var $_lastIndent; + var $_lastNode; + var $_inBlock; + var $_isInline; + var $_dumpIndent; + var $_dumpWordWrap; + + var $_nodeId; + + function _yamlize($key,$value,$indent) { + if (is_array($value)) { + // It has children. What to do? + // Make it the right kind of item + $string = $this->_dumpNode($key,NULL,$indent); + // Add the indent + $indent += $this->_dumpIndent; + // Yamlize the array + $string .= $this->_yamlizeArray($value,$indent); + } elseif ( ! is_array($value)) { + // It doesn't have children. Yip. + $string = $this->_dumpNode($key,$value,$indent); + } + return $string; + } + + function _yamlizeArray($array,$indent) { + if (is_array($array)) { + $string = ''; + foreach ($array as $key => $value) { + $string .= $this->_yamlize($key,$value,$indent); + } + return $string; + } else { + return false; + } + } + + function _dumpNode($key,$value,$indent) { + // do some folding here, for blocks + if (strpos($value,"\n") !== false || strpos($value,": ") !== false || strpos($value,"- ") !== false) { + $value = $this->_doLiteralBlock($value,$indent); + } else { + $value = $this->_doFolding($value,$indent); + } + + if (is_bool($value)) { + $value = ($value) ? "true" : "false"; + } + + $spaces = str_repeat(' ',$indent); + + if (is_int($key)) { + // It's a sequence + $string = $spaces.'- '.$value."\n"; + } else { + // It's mapped + $string = $spaces.$key.': '.$value."\n"; + } + return $string; + } + + function _doLiteralBlock($value,$indent) { + $exploded = explode("\n",$value); + $newValue = '|'; + $indent += $this->_dumpIndent; + $spaces = str_repeat(' ',$indent); + foreach ($exploded as $line) { + $newValue .= "\n" . $spaces . trim($line); + } + return $newValue; + } + + function _doFolding($value,$indent) { + // Don't do anything if wordwrap is set to 0 + if ($this->_dumpWordWrap === 0) { + return $value; + } + + if (strlen($value) > $this->_dumpWordWrap) { + $indent += $this->_dumpIndent; + $indent = str_repeat(' ',$indent); + $wrapped = wordwrap($value,$this->_dumpWordWrap,"\n$indent"); + $value = ">\n".$indent.$wrapped; + } + return $value; + } + + function _getIndent($line) { + preg_match('/^\s{1,}/',$line,$match); + if ( ! empty($match[0])) { + $indent = substr_count($match[0],' '); + } else { + $indent = 0; + } + return $indent; + } + + function _parseLine($line) { + $line = trim($line); + + if(!preg_match("/\\\#/", $line)) { + $line = trim(preg_replace('/#.*$/', '', $line)); + } + + $array = array(); + + if (preg_match('/^-(.*):$/',$line)) { + // It's a mapped sequence + $key = trim(substr(substr($line,1),0,-1)); + $array[$key] = ''; + } elseif ($line[0] == '-' && substr($line,0,3) != '---') { + // It's a list item but not a new stream + if (strlen($line) > 1) { + $value = trim(substr($line,1)); + // Set the type of the value. Int, string, etc + $value = $this->_toType($value); + $array[] = $value; + } else { + $array[] = array(); + } + } elseif (preg_match('/^(.+):/',$line,$key)) { + // It's a key/value pair most likely + // If the key is in double quotes pull it out + if (preg_match('/^(["\'](.*)["\'](\s)*:)/',$line,$matches)) { + $value = trim(str_replace($matches[1],'',$line)); + $key = $matches[2]; + } else { + // Do some guesswork as to the key and the value + $explode = explode(':',$line); + $key = trim($explode[0]); + array_shift($explode); + $value = trim(implode(':',$explode)); + } + + // Set the type of the value. Int, string, etc + $value = $this->_toType($value); + if (empty($key)) { + $array[] = $value; + } else { + $array[$key] = $value; + } + } + return $array; + } + + function _toType($value) { + if (preg_match('/^("(.*)"|\'(.*)\')/',$value,$matches)) { + $value = (string)preg_replace('/(\'\'|\\\\\')/',"'",end($matches)); + $value = preg_replace('/\\\\"/','"',$value); + } elseif (preg_match('/^\\[(.*)\\]$/',$value,$matches)) { + // Inline Sequence + + // Take out strings sequences and mappings + $explode = empty($matches[1]) ? array() : $this->_inlineEscape($matches[1]); + + // Propogate value array + $value = array(); + foreach ($explode as $v) { + $value[] = $this->_toType($v); + } + } elseif (strpos($value,': ')!==false && !preg_match('/^{(.+)/',$value)) { + // It's a map + $array = explode(': ',$value); + $key = trim($array[0]); + array_shift($array); + $value = trim(implode(': ',$array)); + $value = $this->_toType($value); + $value = array($key => $value); + } elseif (preg_match("/{(.+)}$/",$value,$matches)) { + // Inline Mapping + + // Take out strings sequences and mappings + $explode = $this->_inlineEscape($matches[1]); + + // Propogate value array + $array = array(); + foreach ($explode as $v) { + $array = $array + $this->_toType($v); + } + $value = $array; + } elseif (strtolower($value) == 'null' or $value == '' or $value == '~') { + $value = NULL; + } elseif (preg_match ('/^[0-9]+$/', $value)) { + // Cheeky change for compartibility with PHP < 4.2.0 + $raw = $value; + $value = (int) $value; + + if ((string) $value != (string) $raw) { + $value = (string) $raw; + } + } elseif (in_array(strtolower($value), + array('true', 'on', '+', 'yes', 'y'))) { + $value = true; + } elseif (in_array(strtolower($value), + array('false', 'off', '-', 'no', 'n'))) { + $value = false; + } elseif (is_numeric($value)) { + $raw = $value; + $value = (float) $value; + + if ((string) $value != (string) $raw) { + $value = (string) $raw; + } + } else { + // Just a normal string, right? + $value = trim(preg_replace('/#(.+)$/','',$value)); + } + + return $value; + } + + function _inlineEscape($inline) { + // There's gotta be a cleaner way to do this... + // While pure sequences seem to be nesting just fine, + // pure mappings and mappings with sequences inside can't go very + // deep. This needs to be fixed. + + $saved_strings = array(); + + // Check for strings + $regex = '/(?:(")|(?:\'))((?(1)[^"]+|[^\']+))(?(1)"|\')/'; + if (preg_match_all($regex,$inline,$strings)) { + $saved_strings = $strings[0]; + $inline = preg_replace($regex,'YAMLString',$inline); + } + unset($regex); + + // Check for sequences + if (preg_match_all('/\[(.+)\]/U',$inline,$seqs)) { + $inline = preg_replace('/\[(.+)\]/U','YAMLSeq',$inline); + $seqs = $seqs[0]; + } + + // Check for mappings + if (preg_match_all('/{(.+)}/U',$inline,$maps)) { + $inline = preg_replace('/{(.+)}/U','YAMLMap',$inline); + $maps = $maps[0]; + } + + $explode = explode(', ',$inline); + + + // Re-add the sequences + if ( ! empty($seqs)) { + $i = 0; + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLSeq') !== false) { + $explode[$key] = str_replace('YAMLSeq',$seqs[$i],$value); + ++$i; + } + } + } + + // Re-add the mappings + if ( ! empty($maps)) { + $i = 0; + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLMap') !== false) { + $explode[$key] = str_replace('YAMLMap',$maps[$i],$value); + ++$i; + } + } + } + + // Re-add the strings + if ( ! empty($saved_strings)) { + $i = 0; + foreach ($explode as $key => $value) { + while (strpos($value,'YAMLString') !== false) { + $explode[$key] = preg_replace('/YAMLString/',$saved_strings[$i],$value, 1); + ++$i; + $value = $explode[$key]; + } + } + } + + return $explode; + } + + function _buildArray() { + $trunk = array(); + + if ( ! isset($this->_indentSort[0])) { + return $trunk; + } + + foreach ($this->_indentSort[0] as $n) { + if (empty($n->parent)) { + $this->_nodeArrayizeData($n); + // Check for references and copy the needed data to complete them. + $this->_makeReferences($n); + // Merge our data with the big array we're building + $trunk = $this->_array_kmerge($trunk,$n->data); + } + } + + return $trunk; + } + + function _linkReferences() { + if (is_array($this->_haveRefs)) { + foreach ($this->_haveRefs as $node) { + if ( ! empty($node->data)) { + $key = key($node->data); + // If it's an array, don't check. + if (is_array($node->data[$key])) { + foreach ($node->data[$key] as $k => $v) { + $this->_linkRef($node,$key,$k,$v); + } + } else { + $this->_linkRef($node,$key); + } + } + } + } + return true; + } + + function _linkRef(&$n,$key,$k = NULL,$v = NULL) { + if (empty($k) && empty($v)) { + // Look for &refs + if (preg_match('/^&([^ ]+)/',$n->data[$key],$matches)) { + // Flag the node so we know it's a reference + $this->_allNodes[$n->id]->ref = substr($matches[0],1); + $this->_allNodes[$n->id]->data[$key] = + substr($n->data[$key],strlen($matches[0])+1); + // Look for *refs + } elseif (preg_match('/^\*([^ ]+)/',$n->data[$key],$matches)) { + $ref = substr($matches[0],1); + // Flag the node as having a reference + $this->_allNodes[$n->id]->refKey = $ref; + } + } elseif ( ! empty($k) && !empty($v)) { + if (preg_match('/^&([^ ]+)/',$v,$matches)) { + // Flag the node so we know it's a reference + $this->_allNodes[$n->id]->ref = substr($matches[0],1); + $this->_allNodes[$n->id]->data[$key][$k] = + substr($v,strlen($matches[0])+1); + // Look for *refs + } elseif (preg_match('/^\*([^ ]+)/',$v,$matches)) { + $ref = substr($matches[0],1); + // Flag the node as having a reference + $this->_allNodes[$n->id]->refKey = $ref; + } + } + } + + function _gatherChildren($nid) { + $return = array(); + $node =& $this->_allNodes[$nid]; + if (is_array ($this->_allParent[$node->id])) { + foreach ($this->_allParent[$node->id] as $nodeZ) { + $z =& $this->_allNodes[$nodeZ]; + // We found a child + $this->_nodeArrayizeData($z); + // Check for references + $this->_makeReferences($z); + // Merge with the big array we're returning + // The big array being all the data of the children of our parent node + $return = $this->_array_kmerge($return,$z->data); + } + } + return $return; + } + + function _nodeArrayizeData(&$node) { + if (is_array($node->data) && $node->children == true) { + // This node has children, so we need to find them + $childs = $this->_gatherChildren($node->id); + // We've gathered all our children's data and are ready to use it + $key = key($node->data); + $key = empty($key) ? 0 : $key; + // If it's an array, add to it of course + if (isset ($node->data[$key])) { + if (is_array($node->data[$key])) { + $node->data[$key] = $this->_array_kmerge($node->data[$key],$childs); + } else { + $node->data[$key] = $childs; + } + } else { + $node->data[$key] = $childs; + } + } elseif ( ! is_array($node->data) && $node->children == true) { + // Same as above, find the children of this node + $childs = $this->_gatherChildren($node->id); + $node->data = array(); + $node->data[] = $childs; + } + + // We edited $node by reference, so just return true + return true; + } + + function _makeReferences(&$z) { + // It is a reference + if (isset($z->ref)) { + $key = key($z->data); + // Copy the data to this object for easy retrieval later + $this->ref[$z->ref] =& $z->data[$key]; + // It has a reference + } elseif (isset($z->refKey)) { + if (isset($this->ref[$z->refKey])) { + $key = key($z->data); + // Copy the data from this object to make the node a real reference + $z->data[$key] =& $this->ref[$z->refKey]; + } + } + return true; + } + + function _array_kmerge($arr1,$arr2) { + if( ! is_array($arr1)) $arr1 = array(); + if( ! is_array($arr2)) $arr2 = array(); + + $keys = array_merge(array_keys($arr1),array_keys($arr2)); + $vals = array_merge(array_values($arr1),array_values($arr2)); + $ret = array(); + foreach($keys as $key) { + list($unused,$val) = each($vals); + if (isset($ret[$key]) and is_int($key)) $ret[] = $val; else $ret[$key] = $val; + } + return $ret; + } +} \ No newline at end of file diff --git a/ipf/orm/parser/yml.php b/ipf/orm/parser/yml.php new file mode 100644 index 0000000..bd6e0a4 --- /dev/null +++ b/ipf/orm/parser/yml.php @@ -0,0 +1,19 @@ +dump($array, false, false); + return $this->doDump($data, $path); + } + + public function loadData($path) + { + $contents = $this->doLoad($path); + $spyc = new IPF_ORM_Parser_Spyc(); + $array = $spyc->load($contents); + return $array; + } +} diff --git a/ipf/orm/query.php b/ipf/orm/query.php new file mode 100644 index 0000000..29fb618 --- /dev/null +++ b/ipf/orm/query.php @@ -0,0 +1,1494 @@ +_pendingJoinConditions = array(); + $this->_pendingSubqueries = array(); + $this->_pendingFields = array(); + $this->_neededTables = array(); + $this->_expressionMap = array(); + $this->_subqueryAliases = array(); + $this->_needsSubquery = false; + $this->_isLimitSubqueryUsed = false; + } + + public function createSubquery() + { + $class = get_class($this); + $obj = new $class(); + + // copy the aliases to the subquery + $obj->copyAliases($this); + + // this prevents the 'id' being selected, re ticket #307 + $obj->isSubquery(true); + + return $obj; + } + + protected function _addPendingJoinCondition($componentAlias, $joinCondition) + { + $this->_pendingJoins[$componentAlias] = $joinCondition; + } + + public function addEnumParam($key, $table = null, $column = null) + { + $array = (isset($table) || isset($column)) ? array($table, $column) : array(); + + if ($key === '?') { + $this->_enumParams[] = $array; + } else { + $this->_enumParams[$key] = $array; + } + } + + public function getEnumParams() + { + return $this->_enumParams; + } + + public function getDql() + { + $q = ''; + $q .= ( ! empty($this->_dqlParts['select']))? 'SELECT ' . implode(', ', $this->_dqlParts['select']) : ''; + $q .= ( ! empty($this->_dqlParts['from']))? ' FROM ' . implode(' ', $this->_dqlParts['from']) : ''; + $q .= ( ! empty($this->_dqlParts['where']))? ' WHERE ' . implode(' AND ', $this->_dqlParts['where']) : ''; + $q .= ( ! empty($this->_dqlParts['groupby']))? ' GROUP BY ' . implode(', ', $this->_dqlParts['groupby']) : ''; + $q .= ( ! empty($this->_dqlParts['having']))? ' HAVING ' . implode(' AND ', $this->_dqlParts['having']) : ''; + $q .= ( ! empty($this->_dqlParts['orderby']))? ' ORDER BY ' . implode(', ', $this->_dqlParts['orderby']) : ''; + $q .= ( ! empty($this->_dqlParts['limit']))? ' LIMIT ' . implode(' ', $this->_dqlParts['limit']) : ''; + $q .= ( ! empty($this->_dqlParts['offset']))? ' OFFSET ' . implode(' ', $this->_dqlParts['offset']) : ''; + + return $q; + } + + + public function fetchArray($params = array()) { + return $this->execute($params, IPF_ORM::HYDRATE_ARRAY); + } + + public function fetchOne($params = array(), $hydrationMode = null) + { + $collection = $this->execute($params, $hydrationMode); + + if (count($collection) === 0) { + return false; + } + + if ($collection instanceof IPF_ORM_Collection) { + return $collection->getFirst(); + } else if (is_array($collection)) { + return array_shift($collection); + } + + return false; + } + + public function isSubquery($bool = null) + { + if ($bool === null) { + return $this->_isSubquery; + } + + $this->_isSubquery = (bool) $bool; + return $this; + } + + public function getAggregateAlias($dqlAlias) + { + return $this->getSqlAggregateAlias($dqlAlias); + } + + public function getSqlAggregateAlias($dqlAlias) + { + if (isset($this->_aggregateAliasMap[$dqlAlias])) { + // mark the expression as used + $this->_expressionMap[$dqlAlias][1] = true; + + return $this->_aggregateAliasMap[$dqlAlias]; + } else if ( ! empty($this->_pendingAggregates)) { + $this->processPendingAggregates(); + + return $this->getSqlAggregateAlias($dqlAlias); + } else { + throw new IPF_ORM_Exception('Unknown aggregate alias: ' . $dqlAlias); + } + } + + public function getDqlPart($queryPart) + { + if ( ! isset($this->_dqlParts[$queryPart])) { + throw new IPF_ORM_Exception('Unknown query part ' . $queryPart); + } + + return $this->_dqlParts[$queryPart]; + } + + public function contains($dql) + { + return stripos($this->getDql(), $dql) === false ? false : true; + } + + public function processPendingFields($componentAlias) + { + $tableAlias = $this->getTableAlias($componentAlias); + $table = $this->_queryComponents[$componentAlias]['table']; + + if ( ! isset($this->_pendingFields[$componentAlias])) { + if ($this->_hydrator->getHydrationMode() != IPF_ORM::HYDRATE_NONE) { + if ( ! $this->_isSubquery && $componentAlias == $this->getRootAlias()) { + throw new IPF_ORM_Exception("The root class of the query (alias $componentAlias) " + . " must have at least one field selected."); + } + } + return; + } + + // At this point we know the component is FETCHED (either it's the base class of + // the query (FROM xyz) or its a "fetch join"). + + // Check that the parent join (if there is one), is a "fetch join", too. + if (isset($this->_queryComponents[$componentAlias]['parent'])) { + $parentAlias = $this->_queryComponents[$componentAlias]['parent']; + if (is_string($parentAlias) && ! isset($this->_pendingFields[$parentAlias]) + && $this->_hydrator->getHydrationMode() != IPF_ORM::HYDRATE_NONE) { + throw new IPF_ORM_Exception("The left side of the join between " + . "the aliases '$parentAlias' and '$componentAlias' must have at least" + . " the primary key field(s) selected."); + } + } + + $fields = $this->_pendingFields[$componentAlias]; + + // check for wildcards + if (in_array('*', $fields)) { + $fields = $table->getFieldNames(); + } else { + // only auto-add the primary key fields if this query object is not + // a subquery of another query object + if ( ! $this->_isSubquery || $this->_hydrator->getHydrationMode() === IPF_ORM::HYDRATE_NONE) { + $fields = array_unique(array_merge((array) $table->getIdentifier(), $fields)); + } + } + + $sql = array(); + foreach ($fields as $fieldName) { + $columnName = $table->getColumnName($fieldName); + if (($owner = $table->getColumnOwner($columnName)) !== null && + $owner !== $table->getComponentName()) { + + $parent = $this->_conn->getTable($owner); + $columnName = $parent->getColumnName($fieldName); + $parentAlias = $this->getTableAlias($componentAlias . '.' . $parent->getComponentName()); + $sql[] = $this->_conn->quoteIdentifier($parentAlias . '.' . $columnName) + . ' AS ' + . $this->_conn->quoteIdentifier($tableAlias . '__' . $columnName); + } else { + $columnName = $table->getColumnName($fieldName); + $sql[] = $this->_conn->quoteIdentifier($tableAlias . '.' . $columnName) + . ' AS ' + . $this->_conn->quoteIdentifier($tableAlias . '__' . $columnName); + } + } + + $this->_neededTables[] = $tableAlias; + + return implode(', ', $sql); + } + + public function parseSelectField($field) + { + $terms = explode('.', $field); + + if (isset($terms[1])) { + $componentAlias = $terms[0]; + $field = $terms[1]; + } else { + reset($this->_queryComponents); + $componentAlias = key($this->_queryComponents); + $fields = $terms[0]; + } + + $tableAlias = $this->getTableAlias($componentAlias); + $table = $this->_queryComponents[$componentAlias]['table']; + + + // check for wildcards + if ($field === '*') { + $sql = array(); + + foreach ($table->getColumnNames() as $field) { + $sql[] = $this->parseSelectField($componentAlias . '.' . $field); + } + + return implode(', ', $sql); + } else { + $name = $table->getColumnName($field); + + $this->_neededTables[] = $tableAlias; + + return $this->_conn->quoteIdentifier($tableAlias . '.' . $name) + . ' AS ' + . $this->_conn->quoteIdentifier($tableAlias . '__' . $name); + } + } + + public function getExpressionOwner($expr) + { + if (strtoupper(substr(trim($expr, '( '), 0, 6)) !== 'SELECT') { + preg_match_all("/[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*[\.[a-z0-9]+]*/i", $expr, $matches); + + $match = current($matches); + + if (isset($match[0])) { + $terms = explode('.', $match[0]); + + return $terms[0]; + } + } + return $this->getRootAlias(); + + } + + public function parseSelect($dql) + { + $refs = $this->_tokenizer->sqlExplode($dql, ','); + + $pos = strpos(trim($refs[0]), ' '); + $first = substr($refs[0], 0, $pos); + + // check for DISTINCT keyword + if ($first === 'DISTINCT') { + $this->_sqlParts['distinct'] = true; + + $refs[0] = substr($refs[0], ++$pos); + } + + $parsedComponents = array(); + + foreach ($refs as $reference) { + $reference = trim($reference); + + if (empty($reference)) { + continue; + } + + $terms = $this->_tokenizer->sqlExplode($reference, ' '); + + $pos = strpos($terms[0], '('); + + if (count($terms) > 1 || $pos !== false) { + $expression = array_shift($terms); + $alias = array_pop($terms); + + if ( ! $alias) { + $alias = substr($expression, 0, $pos); + } + + $componentAlias = $this->getExpressionOwner($expression); + $expression = $this->parseClause($expression); + + $tableAlias = $this->getTableAlias($componentAlias); + + $index = count($this->_aggregateAliasMap); + + $sqlAlias = $this->_conn->quoteIdentifier($tableAlias . '__' . $index); + + $this->_sqlParts['select'][] = $expression . ' AS ' . $sqlAlias; + + $this->_aggregateAliasMap[$alias] = $sqlAlias; + $this->_expressionMap[$alias][0] = $expression; + + $this->_queryComponents[$componentAlias]['agg'][$index] = $alias; + + $this->_neededTables[] = $tableAlias; + } else { + $e = explode('.', $terms[0]); + + if (isset($e[1])) { + $componentAlias = $e[0]; + $field = $e[1]; + } else { + reset($this->_queryComponents); + $componentAlias = key($this->_queryComponents); + $field = $e[0]; + } + + $this->_pendingFields[$componentAlias][] = $field; + } + } + } + + public function parseClause($clause) + { + $clause = trim($clause); + + if (is_numeric($clause)) { + return $clause; + } + + $terms = $this->_tokenizer->clauseExplode($clause, array(' ', '+', '-', '*', '/', '<', '>', '=', '>=', '<=')); + + $str = ''; + foreach ($terms as $term) { + $pos = strpos($term[0], '('); + + if ($pos !== false) { + $name = substr($term[0], 0, $pos); + $term[0] = $this->parseFunctionExpression($term[0]); + } else { + if (substr($term[0], 0, 1) !== "'" && substr($term[0], -1) !== "'") { + + if (strpos($term[0], '.') !== false) { + if ( ! is_numeric($term[0])) { + $e = explode('.', $term[0]); + + $field = array_pop($e); + + if ($this->getType() === IPF_ORM_Query::SELECT) { + $componentAlias = implode('.', $e); + + if (empty($componentAlias)) { + $componentAlias = $this->getRootAlias(); + } + + $this->load($componentAlias); + + // check the existence of the component alias + if ( ! isset($this->_queryComponents[$componentAlias])) { + throw new IPF_ORM_Exception('Unknown component alias ' . $componentAlias); + } + + $table = $this->_queryComponents[$componentAlias]['table']; + + $def = $table->getDefinitionOf($field); + + // get the actual field name from alias + $field = $table->getColumnName($field); + + // check column existence + if ( ! $def) { + throw new IPF_ORM_Exception('Unknown column ' . $field); + } + + if (isset($def['owner'])) { + $componentAlias = $componentAlias . '.' . $def['owner']; + } + + $tableAlias = $this->getTableAlias($componentAlias); + + // build sql expression + $term[0] = $this->_conn->quoteIdentifier($tableAlias) + . '.' + . $this->_conn->quoteIdentifier($field); + } else { + // build sql expression + $field = $this->getRoot()->getColumnName($field); + $term[0] = $this->_conn->quoteIdentifier($field); + } + } + } else { + if ( ! empty($term[0]) && + ! in_array(strtoupper($term[0]), self::$_keywords) && + ! is_numeric($term[0])) { + + $componentAlias = $this->getRootAlias(); + + $found = false; + + if ($componentAlias !== false && + $componentAlias !== null) { + $table = $this->_queryComponents[$componentAlias]['table']; + + // check column existence + if ($table->hasField($term[0])) { + $found = true; + + $def = $table->getDefinitionOf($term[0]); + + // get the actual column name from field name + $term[0] = $table->getColumnName($term[0]); + + + if (isset($def['owner'])) { + $componentAlias = $componentAlias . '.' . $def['owner']; + } + + $tableAlias = $this->getTableAlias($componentAlias); + + if ($this->getType() === IPF_ORM_Query::SELECT) { + // build sql expression + $term[0] = $this->_conn->quoteIdentifier($tableAlias) + . '.' + . $this->_conn->quoteIdentifier($term[0]); + } else { + // build sql expression + $term[0] = $this->_conn->quoteIdentifier($term[0]); + } + } else { + $found = false; + } + } + + if ( ! $found) { + $term[0] = $this->getSqlAggregateAlias($term[0]); + } + } + } + } + } + + $str .= $term[0] . $term[1]; + } + return $str; + } + + public function parseIdentifierReference($expr) + { + } + + public function parseFunctionExpression($expr) + { + $pos = strpos($expr, '('); + + $name = substr($expr, 0, $pos); + + if ($name === '') { + return $this->parseSubquery($expr); + } + + $argStr = substr($expr, ($pos + 1), -1); + + $args = array(); + // parse args + + foreach ($this->_tokenizer->sqlExplode($argStr, ',') as $arg) { + $args[] = $this->parseClause($arg); + } + + // convert DQL function to its RDBMS specific equivalent + try { + $expr = call_user_func_array(array($this->_conn->expression, $name), $args); + } catch (IPF_ORM_Exception $e) { + throw new IPF_ORM_Exception('Unknown function ' . $name . '.'); + } + + return $expr; + } + + public function parseSubquery($subquery) + { + $trimmed = trim($this->_tokenizer->bracketTrim($subquery)); + + // check for possible subqueries + if (substr($trimmed, 0, 4) == 'FROM' || substr($trimmed, 0, 6) == 'SELECT') { + // parse subquery + $trimmed = $this->createSubquery()->parseDqlQuery($trimmed)->getQuery(); + } else { + // parse normal clause + $trimmed = $this->parseClause($trimmed); + } + + return '(' . $trimmed . ')'; + } + + public function processPendingSubqueries() + { + foreach ($this->_pendingSubqueries as $value) { + list($dql, $alias) = $value; + + $subquery = $this->createSubquery(); + + $sql = $subquery->parseDqlQuery($dql, false)->getQuery(); + + reset($this->_queryComponents); + $componentAlias = key($this->_queryComponents); + $tableAlias = $this->getTableAlias($componentAlias); + + $sqlAlias = $tableAlias . '__' . count($this->_aggregateAliasMap); + + $this->_sqlParts['select'][] = '(' . $sql . ') AS ' . $this->_conn->quoteIdentifier($sqlAlias); + + $this->_aggregateAliasMap[$alias] = $sqlAlias; + $this->_queryComponents[$componentAlias]['agg'][] = $alias; + } + $this->_pendingSubqueries = array(); + } + + public function processPendingAggregates() + { + // iterate trhough all aggregates + foreach ($this->_pendingAggregates as $aggregate) { + list ($expression, $components, $alias) = $aggregate; + + $tableAliases = array(); + + // iterate through the component references within the aggregate function + if ( ! empty ($components)) { + foreach ($components as $component) { + + if (is_numeric($component)) { + continue; + } + + $e = explode('.', $component); + + $field = array_pop($e); + $componentAlias = implode('.', $e); + + // check the existence of the component alias + if ( ! isset($this->_queryComponents[$componentAlias])) { + throw new IPF_ORM_Exception('Unknown component alias ' . $componentAlias); + } + + $table = $this->_queryComponents[$componentAlias]['table']; + + $field = $table->getColumnName($field); + + // check column existence + if ( ! $table->hasColumn($field)) { + throw new IPF_ORM_Exception('Unknown column ' . $field); + } + + $sqlTableAlias = $this->getSqlTableAlias($componentAlias); + + $tableAliases[$sqlTableAlias] = true; + + // build sql expression + + $identifier = $this->_conn->quoteIdentifier($sqlTableAlias . '.' . $field); + $expression = str_replace($component, $identifier, $expression); + } + } + + if (count($tableAliases) !== 1) { + $componentAlias = reset($this->_tableAliasMap); + $tableAlias = key($this->_tableAliasMap); + } + + $index = count($this->_aggregateAliasMap); + $sqlAlias = $this->_conn->quoteIdentifier($tableAlias . '__' . $index); + + $this->_sqlParts['select'][] = $expression . ' AS ' . $sqlAlias; + + $this->_aggregateAliasMap[$alias] = $sqlAlias; + $this->_expressionMap[$alias][0] = $expression; + + $this->_queryComponents[$componentAlias]['agg'][$index] = $alias; + + $this->_neededTables[] = $tableAlias; + } + // reset the state + $this->_pendingAggregates = array(); + } + + protected function _buildSqlQueryBase() + { + switch ($this->_type) { + case self::DELETE: + $q = 'DELETE FROM '; + break; + case self::UPDATE: + $q = 'UPDATE '; + break; + case self::SELECT: + $distinct = ($this->_sqlParts['distinct']) ? 'DISTINCT ' : ''; + $q = 'SELECT ' . $distinct . implode(', ', $this->_sqlParts['select']) . ' FROM '; + break; + } + return $q; + } + + protected function _buildSqlFromPart() + { + $q = ''; + foreach ($this->_sqlParts['from'] as $k => $part) { + if ($k === 0) { + $q .= $part; + continue; + } + + // preserve LEFT JOINs only if needed + // Check if it's JOIN, if not add a comma separator instead of space + if (!preg_match('/\bJOIN\b/i', $part) && !isset($this->_pendingJoinConditions[$k])) { + $q .= ', ' . $part; + } else { + $e = explode(' ', $part); + + if (substr($part, 0, 9) === 'LEFT JOIN') { + $aliases = array_merge($this->_subqueryAliases, + array_keys($this->_neededTables)); + + if ( ! in_array($e[3], $aliases) && + ! in_array($e[2], $aliases) && + + ! empty($this->_pendingFields)) { + continue; + } + + } + + if (isset($this->_pendingJoinConditions[$k])) { + $parser = new IPF_ORM_JoinCondition($this, $this->_tokenizer); + + if (strpos($part, ' ON ') !== false) { + $part .= ' AND '; + } else { + $part .= ' ON '; + } + $part .= $parser->parse($this->_pendingJoinConditions[$k]); + + unset($this->_pendingJoinConditions[$k]); + } + + $tableAlias = trim($e[3], '"'); + $componentAlias = $this->getComponentAlias($tableAlias); + + $string = $this->getInheritanceCondition($componentAlias); + + if ($string) { + $q .= ' ' . $part . ' AND ' . $string; + } else { + $q .= ' ' . $part; + } + } + + $this->_sqlParts['from'][$k] = $part; + } + return $q; + } + + public function getSqlQuery($params = array()) + { + if ($this->_state !== self::STATE_DIRTY) { + return $this->_sql; + } + + // reset the state + if ( ! $this->isSubquery()) { + $this->_queryComponents = array(); + $this->_pendingAggregates = array(); + $this->_aggregateAliasMap = array(); + } + $this->reset(); + + // invoke the preQuery hook + $this->_preQuery(); + + // process the DQL parts => generate the SQL parts. + // this will also populate the $_queryComponents. + foreach ($this->_dqlParts as $queryPartName => $queryParts) { + $this->_processDqlQueryPart($queryPartName, $queryParts); + } + $this->_state = self::STATE_CLEAN; + + $params = $this->convertEnums($params); + + // Proceed with the generated SQL + + if (empty($this->_sqlParts['from'])) { + return false; + } + + $needsSubQuery = false; + $subquery = ''; + $map = reset($this->_queryComponents); + $table = $map['table']; + $rootAlias = key($this->_queryComponents); + + if ( ! empty($this->_sqlParts['limit']) && $this->_needsSubquery && + $table->getAttribute(IPF_ORM::ATTR_QUERY_LIMIT) == IPF_ORM::LIMIT_RECORDS) { + $this->_isLimitSubqueryUsed = true; + $needsSubQuery = true; + } + + $sql = array(); + if ( ! empty($this->_pendingFields)) { + foreach ($this->_queryComponents as $alias => $map) { + $fieldSql = $this->processPendingFields($alias); + if ( ! empty($fieldSql)) { + $sql[] = $fieldSql; + } + } + } + if ( ! empty($sql)) { + array_unshift($this->_sqlParts['select'], implode(', ', $sql)); + } + + $this->_pendingFields = array(); + + // build the basic query + $q = $this->_buildSqlQueryBase(); + $q .= $this->_buildSqlFromPart(); + + if ( ! empty($this->_sqlParts['set'])) { + $q .= ' SET ' . implode(', ', $this->_sqlParts['set']); + } + + $string = $this->getInheritanceCondition($this->getRootAlias()); + + // apply inheritance to WHERE part + if ( ! empty($string)) { + if (substr($string, 0, 1) === '(' && substr($string, -1) === ')') { + $this->_sqlParts['where'][] = $string; + } else { + $this->_sqlParts['where'][] = '(' . $string . ')'; + } + } + + $modifyLimit = true; + if ( ! empty($this->_sqlParts['limit']) || ! empty($this->_sqlParts['offset'])) { + if ($needsSubQuery) { + $subquery = $this->getLimitSubquery(); + // what about composite keys? + $idColumnName = $table->getColumnName($table->getIdentifier()); + switch (strtolower($this->_conn->getDriverName())) { + case 'mysql': + // mysql doesn't support LIMIT in subqueries + $list = $this->_conn->execute($subquery, $params)->fetchAll(IPF_ORM::FETCH_COLUMN); + $subquery = implode(', ', array_map(array($this->_conn, 'quote'), $list)); + break; + case 'pgsql': + // pgsql needs special nested LIMIT subquery + $subquery = 'SELECT ipf_orm_subquery_alias.' . $idColumnName . ' FROM (' . $subquery . ') AS ipf_orm_subquery_alias'; + break; + } + + $field = $this->getSqlTableAlias($rootAlias) . '.' . $idColumnName; + + // only append the subquery if it actually contains something + if ($subquery !== '') { + array_unshift($this->_sqlParts['where'], $this->_conn->quoteIdentifier($field) . ' IN (' . $subquery . ')'); + } + + $modifyLimit = false; + } + } + + $q .= ( ! empty($this->_sqlParts['where']))? ' WHERE ' . implode(' AND ', $this->_sqlParts['where']) : ''; + $q .= ( ! empty($this->_sqlParts['groupby']))? ' GROUP BY ' . implode(', ', $this->_sqlParts['groupby']) : ''; + $q .= ( ! empty($this->_sqlParts['having']))? ' HAVING ' . implode(' AND ', $this->_sqlParts['having']): ''; + $q .= ( ! empty($this->_sqlParts['orderby']))? ' ORDER BY ' . implode(', ', $this->_sqlParts['orderby']) : ''; + + if ($modifyLimit) { + $q = $this->_conn->modifyLimitQuery($q, $this->_sqlParts['limit'], $this->_sqlParts['offset']); + } + + // return to the previous state + if ( ! empty($string)) { + array_pop($this->_sqlParts['where']); + } + if ($needsSubQuery) { + array_shift($this->_sqlParts['where']); + } + $this->_sql = $q; + + return $q; + } + + public function getLimitSubquery() + { + $map = reset($this->_queryComponents); + $table = $map['table']; + $componentAlias = key($this->_queryComponents); + + // get short alias + $alias = $this->getTableAlias($componentAlias); + // what about composite keys? + $primaryKey = $alias . '.' . $table->getColumnName($table->getIdentifier()); + + // initialize the base of the subquery + $subquery = 'SELECT DISTINCT ' . $this->_conn->quoteIdentifier($primaryKey); + + $driverName = $this->_conn->getAttribute(IPF_ORM::ATTR_DRIVER_NAME); + + // pgsql needs the order by fields to be preserved in select clause + if ($driverName == 'pgsql') { + foreach ($this->_sqlParts['orderby'] as $part) { + $part = trim($part); + $e = $this->_tokenizer->bracketExplode($part, ' '); + $part = trim($e[0]); + + if (strpos($part, '.') === false) { + continue; + } + + // don't add functions + if (strpos($part, '(') !== false) { + continue; + } + + // don't add primarykey column (its already in the select clause) + if ($part !== $primaryKey) { + $subquery .= ', ' . $part; + } + } + } + + if ($driverName == 'mysql' || $driverName == 'pgsql') { + foreach ($this->_expressionMap as $dqlAlias => $expr) { + if (isset($expr[1])) { + $subquery .= ', ' . $expr[0] . ' AS ' . $this->_aggregateAliasMap[$dqlAlias]; + } + } + } + + $subquery .= ' FROM'; + + foreach ($this->_sqlParts['from'] as $part) { + // preserve LEFT JOINs only if needed + if (substr($part, 0, 9) === 'LEFT JOIN') { + $e = explode(' ', $part); + + if (empty($this->_sqlParts['orderby']) && empty($this->_sqlParts['where'])) { + continue; + } + } + + $subquery .= ' ' . $part; + } + + // all conditions must be preserved in subquery + $subquery .= ( ! empty($this->_sqlParts['where']))? ' WHERE ' . implode(' AND ', $this->_sqlParts['where']) : ''; + $subquery .= ( ! empty($this->_sqlParts['groupby']))? ' GROUP BY ' . implode(', ', $this->_sqlParts['groupby']) : ''; + $subquery .= ( ! empty($this->_sqlParts['having']))? ' HAVING ' . implode(' AND ', $this->_sqlParts['having']) : ''; + + $subquery .= ( ! empty($this->_sqlParts['orderby']))? ' ORDER BY ' . implode(', ', $this->_sqlParts['orderby']) : ''; + + // add driver specific limit clause + $subquery = $this->_conn->modifyLimitSubquery($table, $subquery, $this->_sqlParts['limit'], $this->_sqlParts['offset']); + + $parts = $this->_tokenizer->quoteExplode($subquery, ' ', "'", "'"); + + foreach ($parts as $k => $part) { + if (strpos($part, ' ') !== false) { + continue; + } + + $part = str_replace(array('"', "'", '`'), "", $part); + + if ($this->hasSqlTableAlias($part)) { + $parts[$k] = $this->_conn->quoteIdentifier($this->generateNewSqlTableAlias($part)); + continue; + } + + if (strpos($part, '.') === false) { + continue; + } + + preg_match_all("/[a-zA-Z0-9_]+\.[a-z0-9_]+/i", $part, $m); + + foreach ($m[0] as $match) { + $e = explode('.', $match); + + // Rebuild the original part without the newly generate alias and with quoting reapplied + $e2 = array(); + foreach ($e as $k2 => $v2) { + $e2[$k2] = $this->_conn->quoteIdentifier($v2); + } + $match = implode('.', $e2); + + // Generate new table alias + $e[0] = $this->generateNewSqlTableAlias($e[0]); + + // Requote the part with the newly generated alias + foreach ($e as $k2 => $v2) { + $e[$k2] = $this->_conn->quoteIdentifier($v2); + } + + $replace = implode('.' , $e); + + // Replace the original part with the new part with new sql table alias + $parts[$k] = str_replace($match, $replace, $parts[$k]); + } + } + + if ($driverName == 'mysql' || $driverName == 'pgsql') { + foreach ($parts as $k => $part) { + if (strpos($part, "'") !== false) { + continue; + } + if (strpos($part, '__') == false) { + continue; + } + + preg_match_all("/[a-zA-Z0-9_]+\_\_[a-z0-9_]+/i", $part, $m); + + foreach ($m[0] as $match) { + $e = explode('__', $match); + $e[0] = $this->generateNewTableAlias($e[0]); + + $parts[$k] = str_replace($match, implode('__', $e), $parts[$k]); + } + } + } + + $subquery = implode(' ', $parts); + return $subquery; + } + + public function parseDqlQuery($query, $clear = true) + { + if ($clear) { + $this->clear(); + } + + $query = trim($query); + $query = str_replace("\r", "\n", str_replace("\r\n", "\n", $query)); + $query = str_replace("\n", ' ', $query); + + $parts = $this->_tokenizer->tokenizeQuery($query); + + foreach ($parts as $partName => $subParts) { + $subParts = trim($subParts); + $partName = strtolower($partName); + switch ($partName) { + case 'create': + $this->_type = self::CREATE; + break; + case 'insert': + $this->_type = self::INSERT; + break; + case 'delete': + $this->_type = self::DELETE; + break; + case 'select': + $this->_type = self::SELECT; + $this->_addDqlQueryPart($partName, $subParts); + break; + case 'update': + $this->_type = self::UPDATE; + $partName = 'from'; + case 'from': + $this->_addDqlQueryPart($partName, $subParts); + break; + case 'set': + $this->_addDqlQueryPart($partName, $subParts, true); + break; + case 'group': + case 'order': + $partName .= 'by'; + case 'where': + case 'having': + case 'limit': + case 'offset': + $this->_addDqlQueryPart($partName, $subParts); + break; + } + } + + return $this; + } + + public function load($path, $loadFields = true) + { + if (isset($this->_queryComponents[$path])) { + return $this->_queryComponents[$path]; + } + + $e = $this->_tokenizer->quoteExplode($path, ' INDEXBY '); + + $mapWith = null; + if (count($e) > 1) { + $mapWith = trim($e[1]); + + $path = $e[0]; + } + + // parse custom join conditions + $e = explode(' ON ', $path); + + $joinCondition = ''; + + if (count($e) > 1) { + $joinCondition = $e[1]; + $overrideJoin = true; + $path = $e[0]; + } else { + $e = explode(' WITH ', $path); + + if (count($e) > 1) { + $joinCondition = $e[1]; + $path = $e[0]; + } + $overrideJoin = false; + } + + $tmp = explode(' ', $path); + $componentAlias = $originalAlias = (count($tmp) > 1) ? end($tmp) : null; + + $e = preg_split("/[.:]/", $tmp[0], -1); + + $fullPath = $tmp[0]; + $prevPath = ''; + $fullLength = strlen($fullPath); + + if (isset($this->_queryComponents[$e[0]])) { + $table = $this->_queryComponents[$e[0]]['table']; + $componentAlias = $e[0]; + + $prevPath = $parent = array_shift($e); + } + + foreach ($e as $key => $name) { + // get length of the previous path + $length = strlen($prevPath); + + // build the current component path + $prevPath = ($prevPath) ? $prevPath . '.' . $name : $name; + + $delimeter = substr($fullPath, $length, 1); + + // if an alias is not given use the current path as an alias identifier + if (strlen($prevPath) === $fullLength && isset($originalAlias)) { + $componentAlias = $originalAlias; + } else { + $componentAlias = $prevPath; + } + + // if the current alias already exists, skip it + if (isset($this->_queryComponents[$componentAlias])) { + throw new IPF_ORM_Exception("Duplicate alias '$componentAlias' in query."); + } + + if ( ! isset($table)) { + // process the root of the path + + $table = $this->loadRoot($name, $componentAlias); + } else { + $join = ($delimeter == ':') ? 'INNER JOIN ' : 'LEFT JOIN '; + + $relation = $table->getRelation($name); + $localTable = $table; + + $table = $relation->getTable(); + $this->_queryComponents[$componentAlias] = array('table' => $table, + 'parent' => $parent, + 'relation' => $relation, + 'map' => null); + if ( ! $relation->isOneToOne()) { + $this->_needsSubquery = true; + } + + $localAlias = $this->getTableAlias($parent, $table->getTableName()); + $foreignAlias = $this->getTableAlias($componentAlias, $relation->getTable()->getTableName()); + + $foreignSql = $this->_conn->quoteIdentifier($relation->getTable()->getTableName()) + . ' ' + . $this->_conn->quoteIdentifier($foreignAlias); + + $map = $relation->getTable()->inheritanceMap; + + if ( ! $loadFields || ! empty($map) || $joinCondition) { + $this->_subqueryAliases[] = $foreignAlias; + } + + if ($relation instanceof IPF_ORM_Relation_Association) { + $asf = $relation->getAssociationTable(); + + $assocTableName = $asf->getTableName(); + + if ( ! $loadFields || ! empty($map) || $joinCondition) { + $this->_subqueryAliases[] = $assocTableName; + } + + $assocPath = $prevPath . '.' . $asf->getComponentName(); + + $this->_queryComponents[$assocPath] = array('parent' => $prevPath, 'relation' => $relation, 'table' => $asf); + + $assocAlias = $this->getTableAlias($assocPath, $asf->getTableName()); + + $queryPart = $join . $assocTableName . ' ' . $assocAlias; + + $queryPart .= ' ON ' . $localAlias + . '.' + . $localTable->getColumnName($localTable->getIdentifier()) // what about composite keys? + . ' = ' + . $assocAlias . '.' . $relation->getLocal(); + + if ($relation->isEqual()) { + // equal nest relation needs additional condition + $queryPart .= ' OR ' . $localAlias + . '.' + . $table->getColumnName($table->getIdentifier()) + . ' = ' + . $assocAlias . '.' . $relation->getForeign(); + } + + $this->_sqlParts['from'][] = $queryPart; + + $queryPart = $join . $foreignSql; + + if ( ! $overrideJoin) { + $queryPart .= $this->buildAssociativeRelationSql($relation, $assocAlias, $foreignAlias, $localAlias); + } + } else { + $queryPart = $this->buildSimpleRelationSql($relation, $foreignAlias, $localAlias, $overrideJoin, $join); + } + + $queryPart .= $this->buildInheritanceJoinSql($table->getComponentName(), $componentAlias); + + $this->_sqlParts['from'][$componentAlias] = $queryPart; + if ( ! empty($joinCondition)) { + $this->_pendingJoinConditions[$componentAlias] = $joinCondition; + } + } + if ($loadFields) { + + $restoreState = false; + // load fields if necessary + if ($loadFields && empty($this->_dqlParts['select'])) { + $this->_pendingFields[$componentAlias] = array('*'); + } + } + $parent = $prevPath; + } + + $table = $this->_queryComponents[$componentAlias]['table']; + + return $this->buildIndexBy($componentAlias, $mapWith); + } + + protected function buildSimpleRelationSql(IPF_ORM_Relation $relation, $foreignAlias, $localAlias, $overrideJoin, $join) + { + $queryPart = $join . $this->_conn->quoteIdentifier($relation->getTable()->getTableName()) + . ' ' + . $this->_conn->quoteIdentifier($foreignAlias); + + if ( ! $overrideJoin) { + $queryPart .= ' ON ' + . $this->_conn->quoteIdentifier($localAlias . '.' . $relation->getLocal()) + . ' = ' + . $this->_conn->quoteIdentifier($foreignAlias . '.' . $relation->getForeign()); + } + + return $queryPart; + } + + protected function buildIndexBy($componentAlias, $mapWith = null) + { + $table = $this->_queryComponents[$componentAlias]['table']; + + $indexBy = null; + + if (isset($mapWith)) { + $terms = explode('.', $mapWith); + + if (isset($terms[1])) { + $indexBy = $terms[1]; + } + } elseif ($table->getBoundQueryPart('indexBy') !== null) { + $indexBy = $table->getBoundQueryPart('indexBy'); + } + + if ($indexBy !== null) { + if ( ! $table->hasColumn($table->getColumnName($indexBy))) { + throw new IPF_ORM_Exception("Couldn't use key mapping. Column " . $indexBy . " does not exist."); + } + + $this->_queryComponents[$componentAlias]['map'] = $indexBy; + } + + return $this->_queryComponents[$componentAlias]; + } + + + protected function buildAssociativeRelationSql(IPF_ORM_Relation $relation, $assocAlias, $foreignAlias, $localAlias) + { + $table = $relation->getTable(); + + $queryPart = ' ON '; + + if ($relation->isEqual()) { + $queryPart .= '('; + } + + $localIdentifier = $table->getColumnName($table->getIdentifier()); + + $queryPart .= $this->_conn->quoteIdentifier($foreignAlias . '.' . $localIdentifier) + . ' = ' + . $this->_conn->quoteIdentifier($assocAlias . '.' . $relation->getForeign()); + + if ($relation->isEqual()) { + $queryPart .= ' OR ' + . $this->_conn->quoteIdentifier($foreignAlias . '.' . $localIdentifier) + . ' = ' + . $this->_conn->quoteIdentifier($assocAlias . '.' . $relation->getLocal()) + . ') AND ' + . $this->_conn->quoteIdentifier($foreignAlias . '.' . $localIdentifier) + . ' != ' + . $this->_conn->quoteIdentifier($localAlias . '.' . $localIdentifier); + } + + return $queryPart; + } + + public function loadRoot($name, $componentAlias) + { + // get the connection for the component + $manager = IPF_ORM_Manager::getInstance(); + if ($manager->hasConnectionForComponent($name)) { + $this->_conn = $manager->getConnectionForComponent($name); + } + + $table = $this->_conn->getTable($name); + $tableName = $table->getTableName(); + + // get the short alias for this table + $tableAlias = $this->getTableAlias($componentAlias, $tableName); + // quote table name + $queryPart = $this->_conn->quoteIdentifier($tableName); + + if ($this->_type === self::SELECT) { + $queryPart .= ' ' . $this->_conn->quoteIdentifier($tableAlias); + } + + $this->_tableAliasMap[$tableAlias] = $componentAlias; + + $queryPart .= $this->buildInheritanceJoinSql($name, $componentAlias); + + $this->_sqlParts['from'][] = $queryPart; + + $this->_queryComponents[$componentAlias] = array('table' => $table, 'map' => null); + + return $table; + } + + public function buildInheritanceJoinSql($name, $componentAlias) + { + // get the connection for the component + $manager = IPF_ORM_Manager::getInstance(); + if ($manager->hasConnectionForComponent($name)) { + $this->_conn = $manager->getConnectionForComponent($name); + } + + $table = $this->_conn->getTable($name); + $tableName = $table->getTableName(); + + // get the short alias for this table + $tableAlias = $this->getTableAlias($componentAlias, $tableName); + + $queryPart = ''; + + foreach ($table->getOption('joinedParents') as $parent) { + $parentTable = $this->_conn->getTable($parent); + + $parentAlias = $componentAlias . '.' . $parent; + + // get the short alias for the parent table + $parentTableAlias = $this->getTableAlias($parentAlias, $parentTable->getTableName()); + + $queryPart .= ' LEFT JOIN ' . $this->_conn->quoteIdentifier($parentTable->getTableName()) + . ' ' . $this->_conn->quoteIdentifier($parentTableAlias) . ' ON '; + + //IPF_ORM::dump($table->getIdentifier()); + foreach ((array) $table->getIdentifier() as $identifier) { + $column = $table->getColumnName($identifier); + + $queryPart .= $this->_conn->quoteIdentifier($tableAlias) + . '.' . $this->_conn->quoteIdentifier($column) + . ' = ' . $this->_conn->quoteIdentifier($parentTableAlias) + . '.' . $this->_conn->quoteIdentifier($column); + } + } + + return $queryPart; + } + + public function getCountQuery() + { + // triggers dql parsing/processing + $this->getQuery(); // this is ugly + + // initialize temporary variables + $where = $this->_sqlParts['where']; + $having = $this->_sqlParts['having']; + $groupby = $this->_sqlParts['groupby']; + $map = reset($this->_queryComponents); + $componentAlias = key($this->_queryComponents); + $tableAlias = $this->getTableAlias($componentAlias); + $table = $map['table']; + $idColumnNames = $table->getIdentifierColumnNames(); + + // build the query base + $q = 'SELECT COUNT(DISTINCT ' . $this->_conn->quoteIdentifier($tableAlias) + . '.' . implode( + ' || ' . $this->_conn->quoteIdentifier($tableAlias) . '.', + $this->_conn->quoteMultipleIdentifier($idColumnNames) + ) . ') AS num_results'; + + foreach ($this->_sqlParts['select'] as $field) { + if (strpos($field, '(') !== false) { + $q .= ', ' . $field; + } + } + + $q .= ' FROM ' . $this->_buildSqlFromPart(); + + // append column aggregation inheritance (if needed) + $string = $this->getInheritanceCondition($this->getRootAlias()); + + if ( ! empty($string)) { + $where[] = $string; + } + + // append conditions + $q .= ( ! empty($where)) ? ' WHERE ' . implode(' AND ', $where) : ''; + + if ( ! empty($groupby)) { + // Maintain existing groupby + $q .= ' GROUP BY ' . implode(', ', $groupby); + } else { + // Default groupby to primary identifier. Database defaults to this internally + // This is required for situations where the user has aggregate functions in the select part + // Without the groupby it fails + $q .= ' GROUP BY ' . $this->_conn->quoteIdentifier($tableAlias) + . '.' . implode( + ', ' . $this->_conn->quoteIdentifier($tableAlias) . '.', + $this->_conn->quoteMultipleIdentifier($idColumnNames) + ); + } + + $q .= ( ! empty($having)) ? ' HAVING ' . implode(' AND ', $having): ''; + + return $q; + } + + public function count($params = array()) + { + $q = $this->getCountQuery(); + + if ( ! is_array($params)) { + $params = array($params); + } + + $params = array_merge($this->_params['join'], $this->_params['where'], $this->_params['having'], $params); + + $params = $this->convertEnums($params); + + $results = $this->getConnection()->fetchAll($q, $params); + + if (count($results) > 1) { + $count = count($results); + } else { + if (isset($results[0])) { + $results[0] = array_change_key_case($results[0], CASE_LOWER); + $count = $results[0]['num_results']; + } else { + $count = 0; + } + } + + return (int) $count; + } + + public function query($query, $params = array(), $hydrationMode = null) + { + $this->parseDqlQuery($query); + return $this->execute($params, $hydrationMode); + } + + public function copy(IPF_ORM_Query $query = null) + { + if ( ! $query) { + $query = $this; + } + + $new = clone $query; + + return $new; + } + + public function __clone() + { + $this->_parsers = array(); + } + + public function free() + { + $this->reset(); + $this->_parsers = array(); + $this->_dqlParts = array(); + $this->_enumParams = array(); + } + + public function serialize() + { + $vars = get_object_vars($this); + } + + public function unserialize($serialized) + { + } +} diff --git a/ipf/orm/query/abstract.php b/ipf/orm/query/abstract.php new file mode 100644 index 0000000..00533c2 --- /dev/null +++ b/ipf/orm/query/abstract.php @@ -0,0 +1,1058 @@ + array(), + 'where' => array(), + 'set' => array(), + 'having' => array()); + + protected $_resultCache; + protected $_expireResultCache = false; + protected $_resultCacheTTL; + + protected $_queryCache; + protected $_expireQueryCache = false; + protected $_queryCacheTTL; + + protected $_conn; + + protected $_sqlParts = array( + 'select' => array(), + 'distinct' => false, + 'forUpdate' => false, + 'from' => array(), + 'set' => array(), + 'join' => array(), + 'where' => array(), + 'groupby' => array(), + 'having' => array(), + 'orderby' => array(), + 'limit' => false, + 'offset' => false, + ); + + protected $_dqlParts = array( + 'from' => array(), + 'select' => array(), + 'forUpdate' => false, + 'set' => array(), + 'join' => array(), + 'where' => array(), + 'groupby' => array(), + 'having' => array(), + 'orderby' => array(), + 'limit' => array(), + 'offset' => array(), + ); + + protected $_queryComponents = array(); + protected $_type = self::SELECT; + protected $_hydrator; + protected $_tokenizer; + protected $_parser; + protected $_tableAliasSeeds = array(); + protected $_options = array( + 'fetchMode' => IPF_ORM::FETCH_RECORD + ); + protected $_enumParams = array(); + + protected $_isLimitSubqueryUsed = false; + protected $_pendingSetParams = array(); + protected $_components; + protected $_preQueried = false; + + public function __construct(IPF_ORM_Connection $connection = null, + IPF_ORM_Hydrator_Abstract $hydrator = null) + { + if ($connection === null) { + $connection = IPF_ORM_Manager::getInstance()->getCurrentConnection(); + } + if ($hydrator === null) { + $hydrator = new IPF_ORM_Hydrator(); + } + $this->_conn = $connection; + $this->_hydrator = $hydrator; + $this->_tokenizer = new IPF_ORM_Query_Tokenizer(); + $this->_resultCacheTTL = $this->_conn->getAttribute(IPF_ORM::ATTR_RESULT_CACHE_LIFESPAN); + $this->_queryCacheTTL = $this->_conn->getAttribute(IPF_ORM::ATTR_QUERY_CACHE_LIFESPAN); + } + + public function setOption($name, $value) + { + if ( ! isset($this->_options[$name])) { + throw new IPF_ORM_Exception('Unknown option ' . $name); + } + $this->_options[$name] = $value; + } + + public function hasTableAlias($sqlTableAlias) + { + return $this->hasSqlTableAlias($sqlTableAlias); + } + + public function hasSqlTableAlias($sqlTableAlias) + { + return (isset($this->_tableAliasMap[$sqlTableAlias])); + } + + public function getTableAliases() + { + return $this->getTableAliasMap(); + } + + public function getTableAliasMap() + { + return $this->_tableAliasMap; + } + + public function getQueryPart($part) + { + return $this->getSqlQueryPart($part); + } + + public function getSqlQueryPart($part) + { + if ( ! isset($this->_sqlParts[$part])) { + throw new IPF_ORM_Exception('Unknown SQL query part ' . $part); + } + return $this->_sqlParts[$part]; + } + + public function setQueryPart($name, $part) + { + return $this->setSqlQueryPart($name, $part); + } + + public function setSqlQueryPart($name, $part) + { + if ( ! isset($this->_sqlParts[$name])) { + throw new IPF_ORM_Exception('Unknown query part ' . $name); + } + + if ($name !== 'limit' && $name !== 'offset') { + if (is_array($part)) { + $this->_sqlParts[$name] = $part; + } else { + $this->_sqlParts[$name] = array($part); + } + } else { + $this->_sqlParts[$name] = $part; + } + + return $this; + } + + public function addQueryPart($name, $part) + { + return $this->addSqlQueryPart($name, $part); + } + + public function addSqlQueryPart($name, $part) + { + if ( ! isset($this->_sqlParts[$name])) { + throw new IPF_ORM_Exception('Unknown query part ' . $name); + } + if (is_array($part)) { + $this->_sqlParts[$name] = array_merge($this->_sqlParts[$name], $part); + } else { + $this->_sqlParts[$name][] = $part; + } + return $this; + } + + public function removeQueryPart($name) + { + return $this->removeSqlQueryPart($name); + } + + public function removeSqlQueryPart($name) + { + if ( ! isset($this->_sqlParts[$name])) { + throw new IPF_ORM_Exception('Unknown query part ' . $name); + } + + if ($name == 'limit' || $name == 'offset') { + $this->_sqlParts[$name] = false; + } else { + $this->_sqlParts[$name] = array(); + } + + return $this; + } + + public function removeDqlQueryPart($name) + { + if ( ! isset($this->_dqlParts[$name])) { + throw new IPF_ORM_Exception('Unknown query part ' . $name); + } + + if ($name == 'limit' || $name == 'offset') { + $this->_dqlParts[$name] = false; + } else { + $this->_dqlParts[$name] = array(); + } + + return $this; + } + + public function getParams($params = array()) + { + return array_merge($this->_params['join'], $this->_params['set'], $this->_params['where'], $this->_params['having'], $params); + } + + public function setParams(array $params = array()) { + $this->_params = $params; + } + + public function setView(IPF_ORM_View $view) + { + $this->_view = $view; + } + + public function getView() + { + return $this->_view; + } + + public function isLimitSubqueryUsed() + { + return $this->_isLimitSubqueryUsed; + } + + public function convertEnums($params) + { + $table = $this->getRoot(); + + // $position tracks the position of the parameter, to ensure we're converting + // the right parameter value when simple ? placeholders are used. + // This only works because SET is only allowed in update statements and it's + // the first place where parameters can occur.. see issue #935 + $position = 0; + foreach ($this->_pendingSetParams as $fieldName => $value) { + $e = explode('.', $fieldName); + $fieldName = isset($e[1]) ? $e[1]:$e[0]; + if ($table->getTypeOf($fieldName) == 'enum') { + $value = $value === '?' ? $position : $value; + $this->addEnumParam($value, $table, $fieldName); + } + ++$position; + } + $this->_pendingSetParams = array(); + + foreach ($this->_enumParams as $key => $values) { + if (isset($params[$key])) { + if ( ! empty($values)) { + $params[$key] = $values[0]->enumIndex($values[1], $params[$key]); + } + } + } + + return $params; + } + + public function getInheritanceCondition($componentAlias) + { + $map = $this->_queryComponents[$componentAlias]['table']->inheritanceMap; + + // No inheritance map so lets just return + if (empty($map)) { + return; + } + + $tableAlias = $this->getSqlTableAlias($componentAlias); + + if ($this->_type !== IPF_ORM_Query::SELECT) { + $tableAlias = ''; + } else { + $tableAlias .= '.'; + } + + $field = key($map); + $value = current($map); + $identifier = $this->_conn->quoteIdentifier($tableAlias . $field); + + return $identifier . ' = ' . $this->_conn->quote($value);; + } + + public function getTableAlias($componentAlias, $tableName = null) + { + return $this->getSqlTableAlias($componentAlias, $tableName); + } + + public function getSqlTableAlias($componentAlias, $tableName = null) + { + $alias = array_search($componentAlias, $this->_tableAliasMap); + + if ($alias !== false) { + return $alias; + } + + if ($tableName === null) { + throw new IPF_ORM_Exception("Couldn't get short alias for " . $componentAlias); + } + + return $this->generateTableAlias($componentAlias, $tableName); + } + + public function generateNewTableAlias($oldAlias) + { + return $this->generateNewSqlTableAlias($oldAlias); + } + + public function generateNewSqlTableAlias($oldAlias) + { + if (isset($this->_tableAliasMap[$oldAlias])) { + // generate a new alias + $name = substr($oldAlias, 0, 1); + $i = ((int) substr($oldAlias, 1)); + + if ($i == 0) { + $i = 1; + } + + $newIndex = ($this->_tableAliasSeeds[$name] + $i); + + return $name . $newIndex; + } + + return $oldAlias; + } + + public function getTableAliasSeed($sqlTableAlias) + { + return $this->getSqlTableAliasSeed($sqlTableAlias); + } + + public function getSqlTableAliasSeed($sqlTableAlias) + { + if ( ! isset($this->_tableAliasSeeds[$sqlTableAlias])) { + return 0; + } + return $this->_tableAliasSeeds[$sqlTableAlias]; + } + + public function hasAliasDeclaration($componentAlias) + { + return isset($this->_queryComponents[$componentAlias]); + } + + public function getAliasDeclaration($componentAlias) + { + return $this->getQueryComponent($componentAlias); + } + + public function getQueryComponent($componentAlias) + { + if ( ! isset($this->_queryComponents[$componentAlias])) { + throw new IPF_ORM_Exception('Unknown component alias ' . $componentAlias); + } + + return $this->_queryComponents[$componentAlias]; + } + + public function copyAliases(IPF_ORM_Query_Abstract $query) + { + $this->_tableAliasMap = $query->_tableAliasMap; + $this->_queryComponents = $query->_queryComponents; + $this->_tableAliasSeeds = $query->_tableAliasSeeds; + return $this; + } + + public function getRootAlias() + { + if ( ! $this->_queryComponents) { + $this->getSql(); + } + reset($this->_queryComponents); + + return key($this->_queryComponents); + } + + public function getRootDeclaration() + { + $map = reset($this->_queryComponents); + return $map; + } + + public function getRoot() + { + $map = reset($this->_queryComponents); + + if ( ! isset($map['table'])) { + throw new IPF_ORM_Exception('Root component not initialized.'); + } + + return $map['table']; + } + + public function generateTableAlias($componentAlias, $tableName) + { + return $this->generateSqlTableAlias($componentAlias, $tableName); + } + + public function generateSqlTableAlias($componentAlias, $tableName) + { + preg_match('/([^_])/', $tableName, $matches); + $char = strtolower($matches[0]); + + $alias = $char; + + if ( ! isset($this->_tableAliasSeeds[$alias])) { + $this->_tableAliasSeeds[$alias] = 1; + } + + while (isset($this->_tableAliasMap[$alias])) { + if ( ! isset($this->_tableAliasSeeds[$alias])) { + $this->_tableAliasSeeds[$alias] = 1; + } + $alias = $char . ++$this->_tableAliasSeeds[$alias]; + } + + $this->_tableAliasMap[$alias] = $componentAlias; + + return $alias; + } + + public function getComponentAlias($sqlTableAlias) + { + if ( ! isset($this->_tableAliasMap[$sqlTableAlias])) { + throw new IPF_ORM_Exception('Unknown table alias ' . $sqlTableAlias); + } + return $this->_tableAliasMap[$sqlTableAlias]; + } + + protected function _execute($params) + { + $params = $this->_conn->convertBooleans($params); + + if ( ! $this->_view) { + if ($this->_queryCache || $this->_conn->getAttribute(IPF_ORM::ATTR_QUERY_CACHE)) { + $queryCacheDriver = $this->getQueryCacheDriver(); + // calculate hash for dql query + $dql = $this->getDql(); + $hash = md5($dql . 'IPF_ORM_QUERY_CACHE_SALT'); + $cached = $queryCacheDriver->fetch($hash); + if ($cached) { + $query = $this->_constructQueryFromCache($cached); + } else { + $query = $this->getSqlQuery($params); + $serializedQuery = $this->getCachedForm($query); + $queryCacheDriver->save($hash, $serializedQuery, $this->getQueryCacheLifeSpan()); + } + } else { + $query = $this->getSqlQuery($params); + } + $params = $this->convertEnums($params); + } else { + $query = $this->_view->getSelectSql(); + } + + if ($this->isLimitSubqueryUsed() && + $this->_conn->getAttribute(IPF_ORM::ATTR_DRIVER_NAME) !== 'mysql') { + $params = array_merge($params, $params); + } + + if ($this->_type !== self::SELECT) { + return $this->_conn->exec($query, $params); + } + + $stmt = $this->_conn->execute($query, $params); + return $stmt; + } + + public function execute($params = array(), $hydrationMode = null) + { + $this->_preQuery(); + + if ($hydrationMode !== null) { + $this->_hydrator->setHydrationMode($hydrationMode); + } + + $params = $this->getParams($params); + + if ($this->_resultCache && $this->_type == self::SELECT) { + $cacheDriver = $this->getResultCacheDriver(); + + $dql = $this->getDql(); + // calculate hash for dql query + $hash = md5($dql . var_export($params, true)); + + $cached = ($this->_expireResultCache) ? false : $cacheDriver->fetch($hash); + + if ($cached === false) { + // cache miss + $stmt = $this->_execute($params); + $this->_hydrator->setQueryComponents($this->_queryComponents); + $result = $this->_hydrator->hydrateResultSet($stmt, $this->_tableAliasMap); + + $cached = $this->getCachedForm($result); + $cacheDriver->save($hash, $cached, $this->getResultCacheLifeSpan()); + } else { + $result = $this->_constructQueryFromCache($cached); + } + } else { + $stmt = $this->_execute($params); + + if (is_integer($stmt)) { + $result = $stmt; + } else { + $this->_hydrator->setQueryComponents($this->_queryComponents); + $result = $this->_hydrator->hydrateResultSet($stmt, $this->_tableAliasMap); + } + } + + return $result; + } + + protected function _getDqlCallback() + { + $callback = false; + if ( ! empty($this->_dqlParts['from'])) { + switch ($this->_type) { + case self::DELETE: + $callback = array( + 'callback' => 'preDqlDelete', + 'const' => IPF_ORM_Event::RECORD_DQL_DELETE + ); + break; + case self::UPDATE: + $callback = array( + 'callback' => 'preDqlUpdate', + 'const' => IPF_ORM_Event::RECORD_DQL_UPDATE + ); + break; + case self::SELECT: + $callback = array( + 'callback' => 'preDqlSelect', + 'const' => IPF_ORM_Event::RECORD_DQL_SELECT + ); + break; + } + } + + return $callback; + } + + protected function _preQuery() + { + if ( ! $this->_preQueried && IPF_ORM_Manager::getInstance()->getAttribute('use_dql_callbacks')) { + $this->_preQueried = true; + + $callback = $this->_getDqlCallback(); + + // if there is no callback for the query type, then we can return early + if ( ! $callback) { + return; + } + + $copy = $this->copy(); + $copy->getSqlQuery(); + + foreach ($copy->getQueryComponents() as $alias => $component) { + $table = $component['table']; + $record = $table->getRecordInstance(); + + // Trigger preDql*() callback event + $params = array('component' => $component, 'alias' => $alias); + $event = new IPF_ORM_Event($record, $callback['const'], $this, $params); + + $record->$callback['callback']($event); + $table->getRecordListener()->$callback['callback']($event); + } + } + + // Invoke preQuery() hook on IPF_ORM_Query for child classes which implement this hook + $this->preQuery(); + } + + public function preQuery() + { + } + + protected function _constructQueryFromCache($cached) + { + $cached = unserialize($cached); + $this->_tableAliasMap = $cached[2]; + $customComponent = $cached[0]; + + $queryComponents = array(); + $cachedComponents = $cached[1]; + foreach ($cachedComponents as $alias => $components) { + $e = explode('.', $components[0]); + if (count($e) === 1) { + $queryComponents[$alias]['table'] = $this->_conn->getTable($e[0]); + } else { + $queryComponents[$alias]['parent'] = $e[0]; + $queryComponents[$alias]['relation'] = $queryComponents[$e[0]]['table']->getRelation($e[1]); + $queryComponents[$alias]['table'] = $queryComponents[$alias]['relation']->getTable(); + } + if (isset($components[1])) { + $queryComponents[$alias]['agg'] = $components[1]; + } + if (isset($components[2])) { + $queryComponents[$alias]['map'] = $components[2]; + } + } + $this->_queryComponents = $queryComponents; + + return $customComponent; + } + + public function getCachedForm($customComponent = null) + { + $componentInfo = array(); + + foreach ($this->getQueryComponents() as $alias => $components) { + if ( ! isset($components['parent'])) { + $componentInfo[$alias][] = $components['table']->getComponentName(); + } else { + $componentInfo[$alias][] = $components['parent'] . '.' . $components['relation']->getAlias(); + } + if (isset($components['agg'])) { + $componentInfo[$alias][] = $components['agg']; + } + if (isset($components['map'])) { + $componentInfo[$alias][] = $components['map']; + } + } + + return serialize(array($customComponent, $componentInfo, $this->getTableAliasMap())); + } + + public function addSelect($select) + { + return $this->_addDqlQueryPart('select', $select, true); + } + + public function addTableAlias($tableAlias, $componentAlias) + { + return $this->addSqlTableAlias($tableAlias, $componentAlias); + } + + public function addSqlTableAlias($sqlTableAlias, $componentAlias) + { + $this->_tableAliasMap[$sqlTableAlias] = $componentAlias; + return $this; + } + + public function addFrom($from) + { + return $this->_addDqlQueryPart('from', $from, true); + } + + public function addWhere($where, $params = array()) + { + if (is_array($params)) { + $this->_params['where'] = array_merge($this->_params['where'], $params); + } else { + $this->_params['where'][] = $params; + } + return $this->_addDqlQueryPart('where', $where, true); + } + + public function whereIn($expr, $params = array(), $not = false) + { + $params = (array) $params; + + // if there's no params, return (else we'll get a WHERE IN (), invalid SQL) + if (!count($params)) + return $this; + + $a = array(); + foreach ($params as $k => $value) { + if ($value instanceof IPF_ORM_Expression) { + $value = $value->getSql(); + unset($params[$k]); + } else { + $value = '?'; + } + $a[] = $value; + } + + $this->_params['where'] = array_merge($this->_params['where'], $params); + + $where = $expr . ($not === true ? ' NOT ':'') . ' IN (' . implode(', ', $a) . ')'; + + return $this->_addDqlQueryPart('where', $where, true); + } + + public function whereNotIn($expr, $params = array()) + { + return $this->whereIn($expr, $params, true); + } + + public function addGroupBy($groupby) + { + return $this->_addDqlQueryPart('groupby', $groupby, true); + } + + public function addHaving($having, $params = array()) + { + if (is_array($params)) { + $this->_params['having'] = array_merge($this->_params['having'], $params); + } else { + $this->_params['having'][] = $params; + } + return $this->_addDqlQueryPart('having', $having, true); + } + + public function addOrderBy($orderby) + { + return $this->_addDqlQueryPart('orderby', $orderby, true); + } + + public function select($select) + { + return $this->_addDqlQueryPart('select', $select); + } + + public function distinct($flag = true) + { + $this->_sqlParts['distinct'] = (bool) $flag; + return $this; + } + + public function forUpdate($flag = true) + { + $this->_sqlParts[self::FOR_UPDATE] = (bool) $flag; + return $this; + } + + public function delete() + { + $this->_type = self::DELETE; + return $this; + } + + public function update($update) + { + $this->_type = self::UPDATE; + return $this->_addDqlQueryPart('from', $update); + } + + public function set($key, $value, $params = null) + { + if (is_array($key)) { + foreach ($key as $k => $v) { + $this->set($k, '?', array($v)); + } + return $this; + } else { + if ($params !== null) { + if (is_array($params)) { + $this->_params['set'] = array_merge($this->_params['set'], $params); + } else { + $this->_params['set'][] = $params; + } + } + + $this->_pendingSetParams[$key] = $value; + + return $this->_addDqlQueryPart('set', $key . ' = ' . $value, true); + } + } + + public function from($from) + { + return $this->_addDqlQueryPart('from', $from); + } + + public function innerJoin($join, $params = array()) + { + if (is_array($params)) { + $this->_params['join'] = array_merge($this->_params['join'], $params); + } else { + $this->_params['join'][] = $params; + } + + return $this->_addDqlQueryPart('from', 'INNER JOIN ' . $join, true); + } + + public function leftJoin($join, $params = array()) + { + if (is_array($params)) { + $this->_params['join'] = array_merge($this->_params['join'], $params); + } else { + $this->_params['join'][] = $params; + } + + return $this->_addDqlQueryPart('from', 'LEFT JOIN ' . $join, true); + } + + public function groupBy($groupby) + { + return $this->_addDqlQueryPart('groupby', $groupby); + } + + public function where($where, $params = array()) + { + $this->_params['where'] = array(); + if (is_array($params)) { + $this->_params['where'] = $params; + } else { + $this->_params['where'][] = $params; + } + + return $this->_addDqlQueryPart('where', $where); + } + + public function having($having, $params = array()) + { + $this->_params['having'] = array(); + if (is_array($params)) { + $this->_params['having'] = $params; + } else { + $this->_params['having'][] = $params; + } + + return $this->_addDqlQueryPart('having', $having); + } + + public function orderBy($orderby) + { + return $this->_addDqlQueryPart('orderby', $orderby); + } + + public function limit($limit) + { + return $this->_addDqlQueryPart('limit', $limit); + } + + public function offset($offset) + { + return $this->_addDqlQueryPart('offset', $offset); + } + + public function getSql($params = array()) + { + return $this->getSqlQuery($params); + } + + protected function clear() + { + $this->_sqlParts = array( + 'select' => array(), + 'distinct' => false, + 'forUpdate' => false, + 'from' => array(), + 'set' => array(), + 'join' => array(), + 'where' => array(), + 'groupby' => array(), + 'having' => array(), + 'orderby' => array(), + 'limit' => false, + 'offset' => false, + ); + } + + public function setHydrationMode($hydrationMode) + { + $this->_hydrator->setHydrationMode($hydrationMode); + return $this; + } + + public function getAliasMap() + { + return $this->_queryComponents; + } + + public function getQueryComponents() + { + return $this->_queryComponents; + } + + public function getParts() + { + return $this->getSqlParts(); + } + + public function getSqlParts() + { + return $this->_sqlParts; + } + + public function getType() + { + return $this->_type; + } + + public function useCache($driver = true, $timeToLive = null) + { + return $this->useResultCache($driver, $timeToLive); + } + + public function useResultCache($driver = true, $timeToLive = null) + { + if ($driver !== null && $driver !== true && ! ($driver instanceOf IPF_ORM_Cache_Interface)) { + $msg = 'First argument should be instance of IPF_ORM_Cache_Interface or null.'; + throw new IPF_ORM_Exception($msg); + } + $this->_resultCache = $driver; + + return $this->setResultCacheLifeSpan($timeToLive); + } + + public function useQueryCache(IPF_ORM_Cache_Interface $driver, $timeToLive = null) + { + $this->_queryCache = $driver; + return $this->setQueryCacheLifeSpan($timeToLive); + } + + public function expireCache($expire = true) + { + return $this->expireResultCache($expire); + } + + public function expireResultCache($expire = true) + { + $this->_expireResultCache = true; + return $this; + } + + public function expireQueryCache($expire = true) + { + $this->_expireQueryCache = true; + return $this; + } + + public function setCacheLifeSpan($timeToLive) + { + return $this->setResultCacheLifeSpan($timeToLive); + } + + public function setResultCacheLifeSpan($timeToLive) + { + if ($timeToLive !== null) { + $timeToLive = (int) $timeToLive; + } + $this->_resultCacheTTL = $timeToLive; + + return $this; + } + + public function getResultCacheLifeSpan() + { + return $this->_resultCacheTTL; + } + + public function setQueryCacheLifeSpan($timeToLive) + { + if ($timeToLive !== null) { + $timeToLive = (int) $timeToLive; + } + $this->_queryCacheTTL = $timeToLive; + + return $this; + } + + public function getQueryCacheLifeSpan() + { + return $this->_queryCacheTTL; + } + + public function getCacheDriver() + { + return $this->getResultCacheDriver(); + } + + public function getResultCacheDriver() + { + if ($this->_resultCache instanceof IPF_ORM_Cache_Interface) { + return $this->_resultCache; + } else { + return $this->_conn->getResultCacheDriver(); + } + } + + public function getQueryCacheDriver() + { + if ($this->_queryCache instanceof IPF_ORM_Cache_Interface) { + return $this->_queryCache; + } else { + return $this->_conn->getQueryCacheDriver(); + } + } + + public function getConnection() + { + return $this->_conn; + } + + protected function _addDqlQueryPart($queryPartName, $queryPart, $append = false) + { + if ($append) { + $this->_dqlParts[$queryPartName][] = $queryPart; + } else { + $this->_dqlParts[$queryPartName] = array($queryPart); + } + + $this->_state = IPF_ORM_Query::STATE_DIRTY; + return $this; + } + + protected function _processDqlQueryPart($queryPartName, $queryParts) + { + $this->removeSqlQueryPart($queryPartName); + + if (is_array($queryParts) && ! empty($queryParts)) { + foreach ($queryParts as $queryPart) { + $parser = $this->_getParser($queryPartName); + $sql = $parser->parse($queryPart); + if (isset($sql)) { + if ($queryPartName == 'limit' || $queryPartName == 'offset') { + $this->setSqlQueryPart($queryPartName, $sql); + } else { + $this->addSqlQueryPart($queryPartName, $sql); + } + } + } + } + } + + protected function _getParser($name) + { + if ( ! isset($this->_parsers[$name])) { + $class = 'IPF_ORM_Query_' . ucwords(strtolower($name)); + + //IPF_ORM::autoload($class); + + if ( ! class_exists($class)) { + throw new IPF_ORM_Exception('Unknown parser ' . $name); + } + + $this->_parsers[$name] = new $class($this, $this->_tokenizer); + } + + return $this->_parsers[$name]; + } + + abstract public function getSqlQuery($params = array()); + + abstract public function parseDqlQuery($query); + + public function parseQuery($query) + { + return $this->parseDqlQuery($query); + } + + public function getQuery($params = array()) + { + return $this->getSqlQuery($params); + } +} diff --git a/ipf/orm/query/check.php b/ipf/orm/query/check.php new file mode 100644 index 0000000..d2d521b --- /dev/null +++ b/ipf/orm/query/check.php @@ -0,0 +1,101 @@ +getCurrentConnection() + ->getTable($table); + } + $this->table = $table; + $this->_tokenizer = new IPF_ORM_Query_Tokenizer(); + } + + public function getTable() + { + return $this->table; + } + + public function parse($dql) + { + $this->sql = $this->parseClause($dql); + } + + public function parseClause($dql) + { + $parts = $this->_tokenizer->sqlExplode($dql, ' AND '); + + if (count($parts) > 1) { + $ret = array(); + foreach ($parts as $part) { + $ret[] = $this->parseSingle($part); + } + + $r = implode(' AND ', $ret); + } else { + $parts = $this->_tokenizer->quoteExplode($dql, ' OR '); + if (count($parts) > 1) { + $ret = array(); + foreach ($parts as $part) { + $ret[] = $this->parseClause($part); + } + + $r = implode(' OR ', $ret); + } else { + $ret = $this->parseSingle($dql); + return $ret; + } + } + return '(' . $r . ')'; + } + + public function parseSingle($part) + { + $e = explode(' ', $part); + + $e[0] = $this->parseFunction($e[0]); + + switch ($e[1]) { + case '>': + case '<': + case '=': + case '!=': + case '<>': + + break; + default: + throw new IPF_ORM_Exception('Unknown operator ' . $e[1]); + } + + return implode(' ', $e); + } + public function parseFunction($dql) + { + if (($pos = strpos($dql, '(')) !== false) { + $func = substr($dql, 0, $pos); + $value = substr($dql, ($pos + 1), -1); + + $expr = $this->table->getConnection()->expression; + + if ( ! method_exists($expr, $func)) { + throw new IPF_ORM_Exception('Unknown function ' . $func); + } + + $func = $expr->$func($value); + } + return $func; + } + + public function getSql() + { + return $this->sql; + } +} \ No newline at end of file diff --git a/ipf/orm/query/condition.php b/ipf/orm/query/condition.php new file mode 100644 index 0000000..5a93f51 --- /dev/null +++ b/ipf/orm/query/condition.php @@ -0,0 +1,71 @@ +_tokenizer->bracketExplode($str, array(' \&\& ', ' AND '), '(', ')'); + + if (count($parts) > 1) { + $ret = array(); + foreach ($parts as $part) { + $part = $this->_tokenizer->bracketTrim($part, '(', ')'); + $ret[] = $this->parse($part); + } + $r = implode(' AND ', $ret); + } else { + + $parts = $this->_tokenizer->bracketExplode($str, array(' \|\| ', ' OR '), '(', ')'); + if (count($parts) > 1) { + $ret = array(); + foreach ($parts as $part) { + $part = $this->_tokenizer->bracketTrim($part, '(', ')'); + $ret[] = $this->parse($part); + } + $r = implode(' OR ', $ret); + } else { + // Fix for #710 + if (substr($parts[0],0,1) == '(' && substr($parts[0], -1) == ')') { + return $this->parse(substr($parts[0], 1, -1)); + } else { + // Processing NOT here + if (strtoupper(substr($parts[0], 0, 4)) === 'NOT ') { + $r = 'NOT ('.$this->parse(substr($parts[0], 4)).')'; + } else { + return $this->load($parts[0]); + } + } + } + } + + return '(' . $r . ')'; + } + + public function parseLiteralValue($value) + { + // check that value isn't a string + if (strpos($value, '\'') === false) { + // parse booleans + $value = $this->query->getConnection() + ->dataDict->parseBoolean($value); + + $a = explode('.', $value); + + if (count($a) > 1) { + // either a float or a component.. + + if ( ! is_numeric($a[0])) { + // a component found + $field = array_pop($a); + $reference = implode('.', $a); + $value = $this->query->getTableAlias($reference). '.' . $field; + } + } + } else { + // string literal found + } + return $value; + } +} \ No newline at end of file diff --git a/ipf/orm/query/filter.php b/ipf/orm/query/filter.php new file mode 100644 index 0000000..65b0917 --- /dev/null +++ b/ipf/orm/query/filter.php @@ -0,0 +1,12 @@ +_filters[] = $filter; + } + + public function get($key) + { + if ( ! isset($this->_filters[$key])) { + throw new IPF_ORM_Exception('Unknown filter ' . $key); + } + return $this->_filters[$key]; + } + + public function set($key, IPF_ORM_Query_Filter $listener) + { + $this->_filters[$key] = $listener; + } + + public function preQuery(IPF_ORM_Query $query) + { + foreach ($this->_filters as $filter) { + $filter->preQuery($query); + } + } + + public function postQuery(IPF_ORM_Query $query) + { + foreach ($this->_filters as $filter) { + $filter->postQuery($query); + } + } +} \ No newline at end of file diff --git a/ipf/orm/query/filter/interface.php b/ipf/orm/query/filter/interface.php new file mode 100644 index 0000000..09c104f --- /dev/null +++ b/ipf/orm/query/filter/interface.php @@ -0,0 +1,7 @@ +_tokenizer->bracketExplode($str, 'JOIN'); + + $from = $return ? array() : null; + + $operator = false; + + switch (trim($parts[0])) { + case 'INNER': + $operator = ':'; + case 'LEFT': + array_shift($parts); + break; + } + + $last = ''; + + foreach ($parts as $k => $part) { + $part = trim($part); + + if (empty($part)) { + continue; + } + + $e = explode(' ', $part); + + if (end($e) == 'INNER' || end($e) == 'LEFT') { + $last = array_pop($e); + } + $part = implode(' ', $e); + + foreach ($this->_tokenizer->bracketExplode($part, ',') as $reference) { + $reference = trim($reference); + $e = explode(' ', $reference); + $e2 = explode('.', $e[0]); + + if ($operator) { + $e[0] = array_shift($e2) . $operator . implode('.', $e2); + } + + if ($return) { + $from[] = $e; + } else { + $table = $this->query->load(implode(' ', $e)); + } + } + + $operator = ($last == 'INNER') ? ':' : '.'; + } + return $from; + } +} diff --git a/ipf/orm/query/groupby.php b/ipf/orm/query/groupby.php new file mode 100644 index 0000000..824f11c --- /dev/null +++ b/ipf/orm/query/groupby.php @@ -0,0 +1,15 @@ +query->parseClause($reference); + } + return implode(', ', $r); + } +} diff --git a/ipf/orm/query/having.php b/ipf/orm/query/having.php new file mode 100644 index 0000000..3fe3b6d --- /dev/null +++ b/ipf/orm/query/having.php @@ -0,0 +1,58 @@ +_tokenizer->bracketExplode($func, ',', '(', ')'); + + foreach ($params as $k => $param) { + $params[$k] = $this->parseAggregateFunction($param); + } + + $funcs = $name . '(' . implode(', ', $params) . ')'; + + return $funcs; + + } else { + if ( ! is_numeric($func)) { + $a = explode('.', $func); + + if (count($a) > 1) { + $field = array_pop($a); + $reference = implode('.', $a); + $map = $this->query->load($reference, false); + $field = $map['table']->getColumnName($field); + $func = $this->query->getTableAlias($reference) . '.' . $field; + } else { + $field = end($a); + $func = $this->query->getAggregateAlias($field); + } + return $func; + } else { + return $func; + } + } + } + + final public function load($having) + { + $tokens = $this->_tokenizer->bracketExplode($having, ' ', '(', ')'); + $part = $this->parseAggregateFunction(array_shift($tokens)); + $operator = array_shift($tokens); + $value = implode(' ', $tokens); + $part .= ' ' . $operator . ' ' . $value; + // check the RHS for aggregate functions + if (strpos($value, '(') !== false) { + $value = $this->parseAggregateFunction($value); + } + return $part; + } +} diff --git a/ipf/orm/query/joincondition.php b/ipf/orm/query/joincondition.php new file mode 100644 index 0000000..94d969b --- /dev/null +++ b/ipf/orm/query/joincondition.php @@ -0,0 +1,126 @@ +_tokenizer->sqlExplode($condition); + + if (count($e) > 2) { + $expr = new IPF_ORM_Expression($e[0], $this->query->getConnection()); + $e[0] = $expr->getSql(); + + $operator = $e[1]; + + if (substr(trim($e[2]), 0, 1) != '(') { + $expr = new IPF_ORM_Expression($e[2], $this->query->getConnection()); + $e[2] = $expr->getSql(); + } + + // We need to check for agg functions here + $hasLeftAggExpression = preg_match('/(.*)\(([^\)]*)\)([\)]*)/', $e[0], $leftMatches); + + if ($hasLeftAggExpression) { + $e[0] = $leftMatches[2]; + } + + $hasRightAggExpression = preg_match('/(.*)\(([^\)]*)\)([\)]*)/', $e[2], $rightMatches); + + if ($hasRightAggExpression) { + $e[2] = $rightMatches[2]; + } + + $a = explode('.', $e[0]); + $field = array_pop($a); + $reference = implode('.', $a); + $value = $e[2]; + + $conn = $this->query->getConnection(); + $alias = $this->query->getTableAlias($reference); + $map = $this->query->getAliasDeclaration($reference); + $table = $map['table']; + // check if value is enumerated value + $enumIndex = $table->enumIndex($field, trim($value, "'")); + + if (false !== $enumIndex && $conn->getAttribute(IPF_ORM::ATTR_USE_NATIVE_ENUM)) { + $enumIndex = $conn->quote($enumIndex, 'text'); + } + + // FIX: Issues with "(" XXX ")" + if ($hasRightAggExpression) { + $value = '(' . $value . ')'; + } + + if (substr($value, 0, 1) == '(') { + // trim brackets + $trimmed = $this->_tokenizer->bracketTrim($value); + + if (substr($trimmed, 0, 4) == 'FROM' || substr($trimmed, 0, 6) == 'SELECT') { + // subquery found + $q = $this->query->createSubquery(); + + // Change due to bug "(" XXX ")" + //$value = '(' . $q->parseQuery($trimmed)->getQuery() . ')'; + $value = $q->parseQuery($trimmed)->getQuery(); + } elseif (substr($trimmed, 0, 4) == 'SQL:') { + // Change due to bug "(" XXX ")" + //$value = '(' . substr($trimmed, 4) . ')'; + $value = substr($trimmed, 4); + } else { + // simple in expression found + $e = $this->_tokenizer->sqlExplode($trimmed, ','); + + $value = array(); + foreach ($e as $part) { + $index = $table->enumIndex($field, trim($part, "'")); + + if (false !== $index && $conn->getAttribute(IPF_ORM::ATTR_USE_NATIVE_ENUM)) { + $index = $conn->quote($index, 'text'); + } + + if ($index !== false) { + $value[] = $index; + } else { + $value[] = $this->parseLiteralValue($part); + } + } + + // Change due to bug "(" XXX ")" + //$value = '(' . implode(', ', $value) . ')'; + $value = implode(', ', $value); + } + } else { + if ($enumIndex !== false) { + $value = $enumIndex; + } else { + $value = $this->parseLiteralValue($value); + } + } + + switch ($operator) { + case '<': + case '>': + case '=': + case '!=': + if ($enumIndex !== false) { + $value = $enumIndex; + } + default: + $leftExpr = (($hasLeftAggExpression) ? $leftMatches[1] . '(' : '') + . $alias . '.' . $field + . (($hasLeftAggExpression) ? $leftMatches[3] . ')' : '') ; + + $rightExpr = (($hasRightAggExpression) ? $rightMatches[1] . '(' : '') + . $value + . (($hasRightAggExpression) ? $rightMatches[3] . ')' : '') ; + + $condition = $leftExpr . ' ' . $operator . ' ' . $rightExpr; + } + + } + + return $condition; + } +} diff --git a/ipf/orm/query/limit.php b/ipf/orm/query/limit.php new file mode 100644 index 0000000..f0cd5ae --- /dev/null +++ b/ipf/orm/query/limit.php @@ -0,0 +1,9 @@ +query->parseClause($r); + + $ret[] = $r; + } + return $ret; + } +} diff --git a/ipf/orm/query/parser.php b/ipf/orm/query/parser.php new file mode 100644 index 0000000..71d6f1b --- /dev/null +++ b/ipf/orm/query/parser.php @@ -0,0 +1,5 @@ +query = $query; + if ( ! $tokenizer) { + $tokenizer = new IPF_ORM_Query_Tokenizer(); + } + $this->_tokenizer = $tokenizer; + } + + public function getQuery() + { + return $this->query; + } +} diff --git a/ipf/orm/query/registry.php b/ipf/orm/query/registry.php new file mode 100644 index 0000000..f35ff42 --- /dev/null +++ b/ipf/orm/query/registry.php @@ -0,0 +1,39 @@ +_queries[$key] = $query; + } else { + // namespace found + $e = explode('/', $key); + + $this->_queries[$e[0]][$e[1]] = $query; + } + } + + public function get($key, $namespace = null) + { + if (isset($namespace)) { + if ( ! isset($this->_queries[$namespace][$key])) { + throw new IPF_ORM_Exception('A query with the name ' . $namespace . '/' . $key . ' does not exist.'); + } + $query = $this->_queries[$namespace][$key]; + } else { + if ( ! isset($this->_queries[$key])) { + throw new IPF_ORM_Exception('A query with the name ' . $key . ' does not exist.'); + } + $query = $this->_queries[$key]; + } + + if ( ! ($query instanceof IPF_ORM_Query)) { + $query = IPF_ORM_Query::create()->parseQuery($query); + } + + return $query; + } +} \ No newline at end of file diff --git a/ipf/orm/query/select.php b/ipf/orm/query/select.php new file mode 100644 index 0000000..0994a68 --- /dev/null +++ b/ipf/orm/query/select.php @@ -0,0 +1,9 @@ +query->parseSelect($dql); + } +} \ No newline at end of file diff --git a/ipf/orm/query/set.php b/ipf/orm/query/set.php new file mode 100644 index 0000000..5e26e3b --- /dev/null +++ b/ipf/orm/query/set.php @@ -0,0 +1,27 @@ +_tokenizer->sqlExplode($dql, ' '); + foreach ($terms as $term) { + preg_match_all("/[a-z0-9_]+\.[a-z0-9_]+[\.[a-z0-9]+]*/i", $term, $m); + + if (isset($m[0])) { + foreach ($m[0] as $part) { + $e = explode('.', trim($part)); + $field = array_pop($e); + + $reference = implode('.', $e); + + $alias = $this->query->getTableAlias($reference); + $map = $this->query->getAliasDeclaration($reference); + + $dql = str_replace($part, $map['table']->getColumnName($field), $dql); + } + } + } + return $dql; + } +} \ No newline at end of file diff --git a/ipf/orm/query/tokenizer.php b/ipf/orm/query/tokenizer.php new file mode 100644 index 0000000..f9a7d13 --- /dev/null +++ b/ipf/orm/query/tokenizer.php @@ -0,0 +1,235 @@ +sqlExplode($query, ' '); + + foreach ($tokens as $index => $token) { + $token = trim($token); + switch (strtolower($token)) { + case 'delete': + case 'update': + case 'select': + case 'set': + case 'from': + case 'where': + case 'limit': + case 'offset': + case 'having': + $p = $token; + //$parts[$token] = array(); + $parts[$token] = ''; + break; + case 'order': + case 'group': + $i = ($index + 1); + if (isset($tokens[$i]) && strtolower($tokens[$i]) === 'by') { + $p = $token; + $parts[$token] = ''; + //$parts[$token] = array(); + } else { + $parts[$p] .= "$token "; + //$parts[$p][] = $token; + } + break; + case 'by': + continue; + default: + if ( ! isset($p)) { + throw new IPF_ORM_Exception( + "Couldn't tokenize query. Encountered invalid token: '$token'."); + } + + $parts[$p] .= "$token "; + //$parts[$p][] = $token; + } + } + return $parts; + } + + public function bracketTrim($str, $e1 = '(', $e2 = ')') + { + if (substr($str, 0, 1) === $e1 && substr($str, -1) === $e2) { + return substr($str, 1, -1); + } else { + return $str; + } + } + + public function bracketExplode($str, $d = ' ', $e1 = '(', $e2 = ')') + { + if (is_array($d)) { + $a = preg_split('#('.implode('|', $d).')#i', $str); + $d = stripslashes($d[0]); + } else { + $a = explode($d, $str); + } + + $i = 0; + $term = array(); + foreach($a as $key=>$val) { + if (empty($term[$i])) { + $term[$i] = trim($val); + $s1 = substr_count($term[$i], $e1); + $s2 = substr_count($term[$i], $e2); + + if ($s1 == $s2) { + $i++; + } + } else { + $term[$i] .= $d . trim($val); + $c1 = substr_count($term[$i], $e1); + $c2 = substr_count($term[$i], $e2); + + if ($c1 == $c2) { + $i++; + } + } + } + return $term; + } + + public function quoteExplode($str, $d = ' ') + { + if (is_array($d)) { + $a = preg_split('/('.implode('|', $d).')/', $str); + $d = stripslashes($d[0]); + } else { + $a = explode($d, $str); + } + + $i = 0; + $term = array(); + foreach ($a as $key => $val) { + if (empty($term[$i])) { + $term[$i] = trim($val); + + if ( ! (substr_count($term[$i], "'") & 1)) { + $i++; + } + } else { + $term[$i] .= $d . trim($val); + + if ( ! (substr_count($term[$i], "'") & 1)) { + $i++; + } + } + } + return $term; + } + + public function sqlExplode($str, $d = ' ', $e1 = '(', $e2 = ')') + { + if ($d == ' ') { + $d = array(' ', '\s'); + } + if (is_array($d)) { + $d = array_map('preg_quote', $d); + + if (in_array(' ', $d)) { + $d[] = '\s'; + } + + $split = '#(' . implode('|', $d) . ')#'; + + $str = preg_split($split, $str); + $d = stripslashes($d[0]); + } else { + $str = explode($d, $str); + } + + $i = 0; + $term = array(); + + foreach ($str as $key => $val) { + if (empty($term[$i])) { + $term[$i] = trim($val); + + $s1 = substr_count($term[$i], $e1); + $s2 = substr_count($term[$i], $e2); + + if (strpos($term[$i], '(') !== false) { + if ($s1 == $s2) { + $i++; + } + } else { + if ( ! (substr_count($term[$i], "'") & 1) && + ! (substr_count($term[$i], "\"") & 1)) { + $i++; + } + } + } else { + $term[$i] .= $d . trim($val); + $c1 = substr_count($term[$i], $e1); + $c2 = substr_count($term[$i], $e2); + + if (strpos($term[$i], '(') !== false) { + if ($c1 == $c2) { + $i++; + } + } else { + if ( ! (substr_count($term[$i], "'") & 1) && + ! (substr_count($term[$i], "\"") & 1)) { + $i++; + } + } + } + } + return $term; + } + + public function clauseExplode($str, array $d, $e1 = '(', $e2 = ')') + { + if (is_array($d)) { + $d = array_map('preg_quote', $d); + + if (in_array(' ', $d)) { + $d[] = '\s'; + } + + $split = '#(' . implode('|', $d) . ')#'; + + $str = preg_split($split, $str, -1, PREG_SPLIT_DELIM_CAPTURE); + } + + $i = 0; + $term = array(); + + foreach ($str as $key => $val) { + if ($key & 1) { + if (isset($term[($i - 1)]) && ! is_array($term[($i - 1)])) { + $term[($i - 1)] = array($term[($i - 1)], $val); + } + continue; + } + if (empty($term[$i])) { + $term[$i] = $val; + } else { + $term[$i] .= $str[($key - 1)] . $val; + } + + $c1 = substr_count($term[$i], $e1); + $c2 = substr_count($term[$i], $e2); + + if (strpos($term[$i], '(') !== false) { + if ($c1 == $c2) { + $i++; + } + } else { + if ( ! (substr_count($term[$i], "'") & 1) && + ! (substr_count($term[$i], "\"") & 1)) { + $i++; + } + } + } + + if (isset($term[$i - 1])) { + $term[$i - 1] = array($term[$i - 1], ''); + } + + return $term; + } +} diff --git a/ipf/orm/query/where.php b/ipf/orm/query/where.php new file mode 100644 index 0000000..1f51bbc --- /dev/null +++ b/ipf/orm/query/where.php @@ -0,0 +1,143 @@ +_tokenizer->bracketTrim(trim($where)); + $conn = $this->query->getConnection(); + $terms = $this->_tokenizer->sqlExplode($where); + + if (count($terms) > 1) { + if (substr($where, 0, 6) == 'EXISTS') { + return $this->parseExists($where, true); + } elseif (substr($where, 0, 10) == 'NOT EXISTS') { + return $this->parseExists($where, false); + } + } + + if (count($terms) < 3) { + $terms = $this->_tokenizer->sqlExplode($where, array('=', '<', '<>', '>', '!=')); + } + + if (count($terms) > 1) { + $first = array_shift($terms); + $value = array_pop($terms); + $operator = trim(substr($where, strlen($first), -strlen($value))); + $table = null; + $field = null; + + if (strpos($first, "'") === false && strpos($first, '(') === false) { + // normal field reference found + $a = explode('.', $first); + + $field = array_pop($a); + $reference = implode('.', $a); + + if (empty($reference)) { + $map = $this->query->getRootDeclaration(); + + $alias = $this->query->getTableAlias($this->query->getRootAlias()); + $table = $map['table']; + } else { + $map = $this->query->load($reference, false); + + $alias = $this->query->getTableAlias($reference); + $table = $map['table']; + } + } + $first = $this->query->parseClause($first); + + $sql = $first . ' ' . $operator . ' ' . $this->parseValue($value, $table, $field); + + return $sql; + } else { + return $where; + } + } + + public function parseValue($value, IPF_ORM_Table $table = null, $field = null) + { + $conn = $this->query->getConnection(); + + if (substr($value, 0, 1) == '(') { + // trim brackets + $trimmed = $this->_tokenizer->bracketTrim($value); + + if (substr($trimmed, 0, 4) == 'FROM' || + substr($trimmed, 0, 6) == 'SELECT') { + + // subquery found + $q = new IPF_ORM_Query(); + $value = '(' . $this->query->createSubquery()->parseQuery($trimmed, false)->getQuery() . ')'; + + } elseif (substr($trimmed, 0, 4) == 'SQL:') { + $value = '(' . substr($trimmed, 4) . ')'; + } else { + // simple in expression found + $e = $this->_tokenizer->sqlExplode($trimmed, ','); + + $value = array(); + + $index = false; + + foreach ($e as $part) { + if (isset($table) && isset($field)) { + $index = $table->enumIndex($field, trim($part, "'")); + + if (false !== $index && $conn->getAttribute(IPF_ORM::ATTR_USE_NATIVE_ENUM)) { + $index = $conn->quote($index, 'text'); + } + } + + if ($index !== false) { + $value[] = $index; + } else { + $value[] = $this->parseLiteralValue($part); + } + } + + $value = '(' . implode(', ', $value) . ')'; + } + } else if (substr($value, 0, 1) == ':' || $value === '?') { + // placeholder found + if (isset($table) && isset($field) && $table->getTypeOf($field) == 'enum') { + $this->query->addEnumParam($value, $table, $field); + } else { + $this->query->addEnumParam($value, null, null); + } + } else { + $enumIndex = false; + if (isset($table) && isset($field)) { + // check if value is enumerated value + $enumIndex = $table->enumIndex($field, trim($value, "'")); + + if (false !== $enumIndex && $conn->getAttribute(IPF_ORM::ATTR_USE_NATIVE_ENUM)) { + $enumIndex = $conn->quote($enumIndex, 'text'); + } + } + + if ($enumIndex !== false) { + $value = $enumIndex; + } else { + $value = $this->parseLiteralValue($value); + } + } + return $value; + } + + public function parseExists($where, $negation) + { + $operator = ($negation) ? 'EXISTS' : 'NOT EXISTS'; + + $pos = strpos($where, '('); + + if ($pos == false) { + throw new IPF_ORM_Exception('Unknown expression, expected a subquery with () -marks'); + } + + $sub = $this->_tokenizer->bracketTrim(substr($where, $pos)); + + return $operator . ' (' . $this->query->createSubquery()->parseQuery($sub, false)->getQuery() . ')'; + } +} diff --git a/ipf/orm/rawsql.php b/ipf/orm/rawsql.php new file mode 100644 index 0000000..4177d05 --- /dev/null +++ b/ipf/orm/rawsql.php @@ -0,0 +1,230 @@ +_parseSelectFields($queryPart); + return $this; + } + if ( ! isset($this->parts[$queryPartName])) { + $this->_sqlParts[$queryPartName] = array(); + } + + if ( ! $append) { + $this->_sqlParts[$queryPartName] = array($queryPart); + } else { + $this->_sqlParts[$queryPartName][] = $queryPart; + } + return $this; + } + + protected function _addDqlQueryPart($queryPartName, $queryPart, $append = false) + { + return $this->parseQueryPart($queryPartName, $queryPart, $append); + } + + private function _parseSelectFields($queryPart){ + preg_match_all('/{([^}{]*)}/U', $queryPart, $m); + $this->fields = $m[1]; + $this->_sqlParts['select'] = array(); + } + + public function parseDqlQuery($query) + { + $this->_parseSelectFields($query); + $this->clear(); + + $tokens = $this->_tokenizer->sqlExplode($query, ' '); + + $parts = array(); + foreach ($tokens as $key => $part) { + $partLowerCase = strtolower($part); + switch ($partLowerCase) { + case 'select': + case 'from': + case 'where': + case 'limit': + case 'offset': + case 'having': + $type = $partLowerCase; + if ( ! isset($parts[$partLowerCase])) { + $parts[$partLowerCase] = array(); + } + break; + case 'order': + case 'group': + $i = $key + 1; + if (isset($tokens[$i]) && strtolower($tokens[$i]) === 'by') { + $type = $partLowerCase . 'by'; + $parts[$type] = array(); + } else { + //not a keyword so we add it to the previous type + $parts[$type][] = $part; + } + break; + case 'by': + continue; + default: + //not a keyword so we add it to the previous type. + if ( ! isset($parts[$type][0])) { + $parts[$type][0] = $part; + } else { + // why does this add to index 0 and not append to the + // array. If it had done that one could have used + // parseQueryPart. + $parts[$type][0] .= ' '.$part; + } + } + } + + $this->_sqlParts = $parts; + $this->_sqlParts['select'] = array(); + + return $this; + } + + public function getSqlQuery($params = array()) + { + $select = array(); + + foreach ($this->fields as $field) { + $e = explode('.', $field); + if ( ! isset($e[1])) { + throw new IPF_ORM_Exception('All selected fields in Sql query must be in format tableAlias.fieldName'); + } + // try to auto-add component + if ( ! $this->hasSqlTableAlias($e[0])) { + try { + $this->addComponent($e[0], ucwords($e[0])); + } catch (IPF_ORM_Exception $exception) { + throw new IPF_ORM_Exception('The associated component for table alias ' . $e[0] . ' couldn\'t be found.'); + } + } + + $componentAlias = $this->getComponentAlias($e[0]); + + if ($e[1] == '*') { + foreach ($this->_queryComponents[$componentAlias]['table']->getColumnNames() as $name) { + $field = $e[0] . '.' . $name; + + $select[$componentAlias][$field] = $field . ' AS ' . $e[0] . '__' . $name; + } + } else { + $field = $e[0] . '.' . $e[1]; + $select[$componentAlias][$field] = $field . ' AS ' . $e[0] . '__' . $e[1]; + } + } + + // force-add all primary key fields + + foreach ($this->getTableAliasMap() as $tableAlias => $componentAlias) { + $map = $this->_queryComponents[$componentAlias]; + + foreach ((array) $map['table']->getIdentifierColumnNames() as $key) { + $field = $tableAlias . '.' . $key; + + if ( ! isset($this->_sqlParts['select'][$field])) { + $select[$componentAlias][$field] = $field . ' AS ' . $tableAlias . '__' . $key; + } + } + } + + // first add the fields of the root component + reset($this->_queryComponents); + $componentAlias = key($this->_queryComponents); + + $q = 'SELECT ' . implode(', ', $select[$componentAlias]); + unset($select[$componentAlias]); + + foreach ($select as $component => $fields) { + if ( ! empty($fields)) { + $q .= ', ' . implode(', ', $fields); + } + } + + $string = $this->getInheritanceCondition($this->getRootAlias()); + if ( ! empty($string)) { + $this->_sqlParts['where'][] = $string; + } + $copy = $this->_sqlParts; + unset($copy['select']); + + $q .= ( ! empty($this->_sqlParts['from']))? ' FROM ' . implode(' ', $this->_sqlParts['from']) : ''; + $q .= ( ! empty($this->_sqlParts['where']))? ' WHERE ' . implode(' AND ', $this->_sqlParts['where']) : ''; + $q .= ( ! empty($this->_sqlParts['groupby']))? ' GROUP BY ' . implode(', ', $this->_sqlParts['groupby']) : ''; + $q .= ( ! empty($this->_sqlParts['having']))? ' HAVING ' . implode(' AND ', $this->_sqlParts['having']) : ''; + $q .= ( ! empty($this->_sqlParts['orderby']))? ' ORDER BY ' . implode(', ', $this->_sqlParts['orderby']) : ''; + $q .= ( ! empty($this->_sqlParts['limit']))? ' LIMIT ' . implode(' ', $this->_sqlParts['limit']) : ''; + $q .= ( ! empty($this->_sqlParts['offset']))? ' OFFSET ' . implode(' ', $this->_sqlParts['offset']) : ''; + + if ( ! empty($string)) { + array_pop($this->_sqlParts['where']); + } + return $q; + } + + public function getFields() + { + return $this->fields; + } + + public function addComponent($tableAlias, $path) + { + $tmp = explode(' ', $path); + $originalAlias = (count($tmp) > 1) ? end($tmp) : null; + + $e = explode('.', $tmp[0]); + + $fullPath = $tmp[0]; + $fullLength = strlen($fullPath); + + $table = null; + + $currPath = ''; + + if (isset($this->_queryComponents[$e[0]])) { + $table = $this->_queryComponents[$e[0]]['table']; + + $currPath = $parent = array_shift($e); + } + + foreach ($e as $k => $component) { + // get length of the previous path + $length = strlen($currPath); + + // build the current component path + $currPath = ($currPath) ? $currPath . '.' . $component : $component; + + $delimeter = substr($fullPath, $length, 1); + + // if an alias is not given use the current path as an alias identifier + if (strlen($currPath) === $fullLength && isset($originalAlias)) { + $componentAlias = $originalAlias; + } else { + $componentAlias = $currPath; + } + if ( ! isset($table)) { + $conn = IPF_ORM_Manager::getInstance() + ->getConnectionForComponent($component); + + $table = $conn->getTable($component); + $this->_queryComponents[$componentAlias] = array('table' => $table); + } else { + $relation = $table->getRelation($component); + + $this->_queryComponents[$componentAlias] = array('table' => $relation->getTable(), + 'parent' => $parent, + 'relation' => $relation); + } + $this->addSqlTableAlias($tableAlias, $componentAlias); + + $parent = $currPath; + } + + return $this; + } +} \ No newline at end of file diff --git a/ipf/orm/record.php b/ipf/orm/record.php new file mode 100644 index 0000000..325e42d --- /dev/null +++ b/ipf/orm/record.php @@ -0,0 +1,1266 @@ +_table = $table; + $exists = ( ! $isNewEntry); + } else { + // get the table of this class + $class = get_class($this); + $this->_table = IPF_ORM::getTable($class); + $exists = false; + } + + // Check if the current connection has the records table in its registry + // If not this record is only used for creating table definition and setting up + // relations. + if ( ! $this->_table->getConnection()->hasTable($this->_table->getComponentName())) { + return; + } + + $this->_oid = self::$_index; + + self::$_index++; + + // get the data array + $this->_data = $this->_table->getData(); + + // get the column count + $count = count($this->_data); + + $this->_values = $this->cleanData($this->_data); + + $this->prepareIdentifiers($exists); + + if ( ! $exists) { + if ($count > count($this->_values)) { + $this->_state = IPF_ORM_Record::STATE_TDIRTY; + } else { + $this->_state = IPF_ORM_Record::STATE_TCLEAN; + } + + // set the default values for this record + $this->assignDefaultValues(); + } else { + $this->_state = IPF_ORM_Record::STATE_CLEAN; + + if ($count < $this->_table->getColumnCount()) { + $this->_state = IPF_ORM_Record::STATE_PROXY; + } + } + + $repository = $this->_table->getRepository(); + $repository->add($this); + + $this->construct(); + } + + public static function _index() + { + return self::$_index; + } + + public function setUp(){} + public function construct(){} + + public function getOid() + { + return $this->_oid; + } + + public function oid() + { + return $this->_oid; + } + + public function isValid() + { + if ( ! $this->_table->getAttribute(IPF_ORM::ATTR_VALIDATE)) { + return true; + } + // Clear the stack from any previous errors. + $this->getErrorStack()->clear(); + + // Run validation process + $validator = new IPF_ORM_Validator(); + $validator->validateRecord($this); + $this->validate(); + if ($this->_state == self::STATE_TDIRTY || $this->_state == self::STATE_TCLEAN) { + $this->validateOnInsert(); + } else { + $this->validateOnUpdate(); + } + + return $this->getErrorStack()->count() == 0 ? true : false; + } + + protected function validate(){} + + protected function validateOnUpdate(){} + + protected function validateOnInsert(){} + + public function preSerialize($event){} + + public function postSerialize($event){} + + public function preUnserialize($event){} + + public function postUnserialize($event){} + + public function preSave($event){} + + public function postSave($event){} + + public function preDelete($event){} + + public function postDelete($event){} + + public function preUpdate($event){} + + public function postUpdate($event){} + + public function preInsert($event){} + + public function postInsert($event){} + + public function preDqlSelect($event){} + + public function preDqlUpdate($event){} + + public function preDqlDelete($event){} + + public function getErrorStack(){ + if ( ! $this->_errorStack) { + $this->_errorStack = new IPF_ORM_Validator_ErrorStack(get_class($this)); + } + + return $this->_errorStack; + } + + public function errorStack($stack = null) + { + if ($stack !== null) { + if ( ! ($stack instanceof IPF_ORM_Validator_ErrorStack)) { + throw new IPF_ORM_Exception('Argument should be an instance of IPF_ORM_Validator_ErrorStack.'); + } + $this->_errorStack = $stack; + } else { + return $this->getErrorStack(); + } + } + + public function assignDefaultValues($overwrite = false) + { + if ( ! $this->_table->hasDefaultValues()) { + return false; + } + foreach ($this->_data as $column => $value) { + $default = $this->_table->getDefaultValueOf($column); + + if ($default === null) { + continue; + } + + if ($value === self::$_null || $overwrite) { + $this->_data[$column] = $default; + $this->_modified[] = $column; + $this->_state = IPF_ORM_Record::STATE_TDIRTY; + } + } + } + + public function cleanData(&$data) + { + $tmp = $data; + $data = array(); + + foreach ($this->getTable()->getFieldNames() as $fieldName) { + if (isset($tmp[$fieldName])) { + $data[$fieldName] = $tmp[$fieldName]; + } else if (array_key_exists($fieldName, $tmp)) { + $data[$fieldName] = self::$_null; + } else if (!isset($this->_data[$fieldName])) { + $data[$fieldName] = self::$_null; + } + unset($tmp[$fieldName]); + } + + return $tmp; + } + + public function hydrate(array $data) + { + $this->_values = array_merge($this->_values, $this->cleanData($data)); + $this->_data = array_merge($this->_data, $data); + $this->prepareIdentifiers(true); + } + + private function prepareIdentifiers($exists = true) + { + switch ($this->_table->getIdentifierType()) { + case IPF_ORM::IDENTIFIER_AUTOINC: + case IPF_ORM::IDENTIFIER_SEQUENCE: + case IPF_ORM::IDENTIFIER_NATURAL: + $name = $this->_table->getIdentifier(); + if (is_array($name)) { + $name = $name[0]; + } + if ($exists) { + if (isset($this->_data[$name]) && $this->_data[$name] !== self::$_null) { + $this->_id[$name] = $this->_data[$name]; + } + } + break; + case IPF_ORM::IDENTIFIER_COMPOSITE: + $names = $this->_table->getIdentifier(); + + foreach ($names as $name) { + if ($this->_data[$name] === self::$_null) { + $this->_id[$name] = null; + } else { + $this->_id[$name] = $this->_data[$name]; + } + } + break; + } + } + + public function serialize() + { + $event = new IPF_ORM_Event($this, IPF_ORM_Event::RECORD_SERIALIZE); + + $this->preSerialize($event); + + $vars = get_object_vars($this); + + unset($vars['_references']); + unset($vars['_table']); + unset($vars['_errorStack']); + unset($vars['_filter']); + unset($vars['_node']); + + $name = $this->_table->getIdentifier(); + $this->_data = array_merge($this->_data, $this->_id); + + foreach ($this->_data as $k => $v) { + if ($v instanceof IPF_ORM_Record && $this->_table->getTypeOf($k) != 'object') { + unset($vars['_data'][$k]); + } elseif ($v === self::$_null) { + unset($vars['_data'][$k]); + } else { + switch ($this->_table->getTypeOf($k)) { + case 'array': + case 'object': + $vars['_data'][$k] = serialize($vars['_data'][$k]); + break; + case 'gzip': + $vars['_data'][$k] = gzcompress($vars['_data'][$k]); + break; + case 'enum': + $vars['_data'][$k] = $this->_table->enumIndex($k, $vars['_data'][$k]); + break; + } + } + } + + $str = serialize($vars); + + $this->postSerialize($event); + + return $str; + } + + public function unserialize($serialized) + { + $event = new IPF_ORM_Event($this, IPF_ORM_Event::RECORD_UNSERIALIZE); + + $this->preUnserialize($event); + + $manager = IPF_ORM_Manager::getInstance(); + $connection = $manager->getConnectionForComponent(get_class($this)); + + $this->_oid = self::$_index; + self::$_index++; + + $this->_table = $connection->getTable(get_class($this)); + + $array = unserialize($serialized); + + foreach($array as $k => $v) { + $this->$k = $v; + } + + foreach ($this->_data as $k => $v) { + switch ($this->_table->getTypeOf($k)) { + case 'array': + case 'object': + $this->_data[$k] = unserialize($this->_data[$k]); + break; + case 'gzip': + $this->_data[$k] = gzuncompress($this->_data[$k]); + break; + case 'enum': + $this->_data[$k] = $this->_table->enumValue($k, $this->_data[$k]); + break; + + } + } + + $this->_table->getRepository()->add($this); + + $this->cleanData($this->_data); + + $this->prepareIdentifiers($this->exists()); + + $this->postUnserialize($event); + } + + public function state($state = null) + { + if ($state == null) { + return $this->_state; + } + + $err = false; + if (is_integer($state)) { + if ($state >= 1 && $state <= 6) { + $this->_state = $state; + } else { + $err = true; + } + } else if (is_string($state)) { + $upper = strtoupper($state); + + $const = 'IPF_ORM_Record::STATE_' . $upper; + if (defined($const)) { + $this->_state = constant($const); + } else { + $err = true; + } + } + + if ($this->_state === IPF_ORM_Record::STATE_TCLEAN || + $this->_state === IPF_ORM_Record::STATE_CLEAN) { + $this->_modified = array(); + } + + if ($err) { + throw new IPF_ORM_Exception('Unknown record state ' . $state); + } + } + + public function refresh($deep = false) + { + $id = $this->identifier(); + if ( ! is_array($id)) { + $id = array($id); + } + if (empty($id)) { + return false; + } + $id = array_values($id); + + if ($deep) { + $query = $this->getTable()->createQuery(); + foreach (array_keys($this->_references) as $name) { + $query->leftJoin(get_class($this) . '.' . $name); + } + $query->where(implode(' = ? AND ', $this->getTable()->getIdentifierColumnNames()) . ' = ?'); + $this->clearRelated(); + $record = $query->fetchOne($id); + } else { + // Use FETCH_ARRAY to avoid clearing object relations + $record = $this->getTable()->find($id, IPF_ORM::HYDRATE_ARRAY); + if ($record) { + $this->hydrate($record); + } + } + + if ($record === false) { + throw new IPF_ORM_Exception('Failed to refresh. Record does not exist.'); + } + + $this->_modified = array(); + + $this->prepareIdentifiers(); + + $this->_state = IPF_ORM_Record::STATE_CLEAN; + + return $this; + } + + public function refreshRelated($name = null) + { + if (is_null($name)) { + foreach ($this->_table->getRelations() as $rel) { + $this->_references[$rel->getAlias()] = $rel->fetchRelatedFor($this); + } + } else { + $rel = $this->_table->getRelation($name); + $this->_references[$name] = $rel->fetchRelatedFor($this); + } + } + + public function clearRelated() + { + $this->_references = array(); + } + + public function getTable() + { + return $this->_table; + } + + public function getData() + { + return $this->_data; + } + + public function rawGet($fieldName) + { + if ( ! isset($this->_data[$fieldName])) { + throw new IPF_ORM_Exception('Unknown property '. $fieldName); + } + if ($this->_data[$fieldName] === self::$_null) { + return null; + } + + return $this->_data[$fieldName]; + } + + public function load() + { + // only load the data from database if the IPF_ORM_Record is in proxy state + if ($this->_state == IPF_ORM_Record::STATE_PROXY) { + $this->refresh(); + $this->_state = IPF_ORM_Record::STATE_CLEAN; + return true; + } + return false; + } + + public function get($fieldName, $load = true) + { + $value = self::$_null; + + if (isset($this->_data[$fieldName])) { + // check if the value is the IPF_ORM_Null object located in self::$_null) + if ($this->_data[$fieldName] === self::$_null && $load) { + $this->load(); + } + if ($this->_data[$fieldName] === self::$_null) { + $value = null; + } else { + $value = $this->_data[$fieldName]; + } + return $value; + } + + if (isset($this->_values[$fieldName])) { + return $this->_values[$fieldName]; + } + + try { + if ( ! isset($this->_references[$fieldName]) && $load) { + $rel = $this->_table->getRelation($fieldName); + $this->_references[$fieldName] = $rel->fetchRelatedFor($this); + } + return $this->_references[$fieldName]; + } catch (IPF_ORM_Exception_Table $e) { + foreach ($this->_table->getFilters() as $filter) { + if (($value = $filter->filterGet($this, $fieldName, $value)) !== null) { + return $value; + } + } + } + } + + public function mapValue($name, $value) + { + $this->_values[$name] = $value; + } + + public function set($fieldName, $value, $load = true) + { + if (isset($this->_data[$fieldName])) { + $type = $this->_table->getTypeOf($fieldName); + if ($value instanceof IPF_ORM_Record) { + $id = $value->getIncremented(); + + if ($id !== null && $type !== 'object') { + $value = $id; + } + } + + if ($load) { + $old = $this->get($fieldName, $load); + } else { + $old = $this->_data[$fieldName]; + } + + if ($this->_isValueModified($type, $old, $value)) { + if ($value === null) { + $value = self::$_null; + } + + $this->_data[$fieldName] = $value; + $this->_modified[] = $fieldName; + switch ($this->_state) { + case IPF_ORM_Record::STATE_CLEAN: + $this->_state = IPF_ORM_Record::STATE_DIRTY; + break; + case IPF_ORM_Record::STATE_TCLEAN: + $this->_state = IPF_ORM_Record::STATE_TDIRTY; + break; + } + } + } else { + try { + $this->coreSetRelated($fieldName, $value); + } catch (IPF_ORM_Exception_Table $e) { + foreach ($this->_table->getFilters() as $filter) { + if (($value = $filter->filterSet($this, $fieldName, $value)) !== null) { + break; + } + } + } + } + + return $this; + } + + protected function _isValueModified($type, $old, $new) + { + if ($type == 'boolean' && (is_bool($old) || is_numeric($old)) && (is_bool($new) || is_numeric($new)) && $old == $new) { + return false; + } else { + return $old !== $new; + } + } + + public function coreSetRelated($name, $value) + { + $rel = $this->_table->getRelation($name); + + if ($value === null) { + $value = self::$_null; + } + + // one-to-many or one-to-one relation + if ($rel instanceof IPF_ORM_Relation_ForeignKey || $rel instanceof IPF_ORM_Relation_LocalKey) { + if ( ! $rel->isOneToOne()) { + // one-to-many relation found + if ( ! ($value instanceof IPF_ORM_Collection)) { + throw new IPF_ORM_Exception("Couldn't call IPF_ORM::set(), second argument should be an instance of IPF_ORM_Collection when setting one-to-many references."); + } + if (isset($this->_references[$name])) { + $this->_references[$name]->setData($value->getData()); + return $this; + } + } else { + if ($value !== self::$_null) { + $relatedTable = $value->getTable(); + $foreignFieldName = $relatedTable->getFieldName($rel->getForeign()); + $localFieldName = $this->_table->getFieldName($rel->getLocal()); + + // one-to-one relation found + if ( ! ($value instanceof IPF_ORM_Record)) { + throw new IPF_ORM_Exception("Couldn't call IPF_ORM::set(), second argument should be an instance of IPF_ORM_Record or IPF_ORM_Null when setting one-to-one references."); + } + if ($rel instanceof IPF_ORM_Relation_LocalKey) { + if ( ! empty($foreignFieldName) && $foreignFieldName != $value->getTable()->getIdentifier()) { + $this->set($localFieldName, $value->rawGet($foreignFieldName), false); + } else { + $this->set($localFieldName, $value, false); + } + } else { + $value->set($foreignFieldName, $this, false); + } + } + } + + } else if ($rel instanceof IPF_ORM_Relation_Association) { + // join table relation found + if ( ! ($value instanceof IPF_ORM_Collection)) { + throw new IPF_ORM_Exception("Couldn't call IPF_ORM::set(), second argument should be an instance of IPF_ORM_Collection when setting many-to-many references."); + } + } + + $this->_references[$name] = $value; + } + + public function contains($fieldName) + { + if (isset($this->_data[$fieldName])) { + // this also returns true if the field is a IPF_ORM_Null. + // imho this is not correct behavior. + return true; + } + if (isset($this->_id[$fieldName])) { + return true; + } + if (isset($this->_values[$fieldName])) { + return true; + } + if (isset($this->_references[$fieldName]) && + $this->_references[$fieldName] !== self::$_null) { + + return true; + } + return false; + } + + public function __unset($name) + { + if (isset($this->_data[$name])) { + $this->_data[$name] = array(); + } else if (isset($this->_references[$name])) { + if ($this->_references[$name] instanceof IPF_ORM_Record) { + $this->_pendingDeletes[] = $this->$name; + $this->_references[$name] = self::$_null; + } elseif ($this->_references[$name] instanceof IPF_ORM_Collection) { + $this->_pendingDeletes[] = $this->$name; + $this->_references[$name]->setData(array()); + } + } + } + + public function getPendingDeletes() + { + return $this->_pendingDeletes; + } + + public function save(IPF_ORM_Connection $conn = null) + { + if ($conn === null) { + $conn = $this->_table->getConnection(); + } + $conn->unitOfWork->saveGraph($this); + } + + public function trySave(IPF_ORM_Connection $conn = null) { + try { + $this->save($conn); + return true; + } catch (IPF_ORM_Exception_Validator $ignored) { + return false; + } + } + + public function replace(IPF_ORM_Connection $conn = null) + { + if ($conn === null) { + $conn = $this->_table->getConnection(); + } + + if ($this->exists()) { + return $this->save(); + } else { + $identifier = (array) $this->getTable()->getIdentifier(); + return $conn->replace($this->_table, $this->toArray(), $identifier); + } + } + + public function getModified() + { + $a = array(); + + foreach ($this->_modified as $k => $v) { + $a[$v] = $this->_data[$v]; + } + return $a; + } + + public function modifiedFields() + { + $a = array(); + + foreach ($this->_modified as $k => $v) { + $a[$v] = $this->_data[$v]; + } + return $a; + } + + public function getPrepared(array $array = array()) + { + $a = array(); + + if (empty($array)) { + $modifiedFields = $this->_modified; + } + + foreach ($modifiedFields as $field) { + $type = $this->_table->getTypeOf($field); + + if ($this->_data[$field] === self::$_null) { + $a[$field] = null; + continue; + } + + switch ($type) { + case 'array': + case 'object': + $a[$field] = serialize($this->_data[$field]); + break; + case 'gzip': + $a[$field] = gzcompress($this->_data[$field],5); + break; + case 'boolean': + $a[$field] = $this->getTable()->getConnection()->convertBooleans($this->_data[$field]); + break; + case 'enum': + $a[$field] = $this->_table->enumIndex($field, $this->_data[$field]); + break; + default: + if ($this->_data[$field] instanceof IPF_ORM_Record) { + $a[$field] = $this->_data[$field]->getIncremented(); + if ($a[$field] !== null) { + $this->_data[$field] = $a[$field]; + } + } else { + $a[$field] = $this->_data[$field]; + } + /** TODO: + if ($this->_data[$v] === null) { + throw new IPF_ORM_Record_Exception('Unexpected null value.'); + } + */ + } + } + $map = $this->_table->inheritanceMap; + foreach ($map as $k => $v) { + $k = $this->_table->getFieldName($k); + $old = $this->get($k, false); + + if ((string) $old !== (string) $v || $old === null) { + $a[$k] = $v; + $this->_data[$k] = $v; + } + } + + return $a; + } + + public function count() + { + return count($this->_data); + } + + public function columnCount() + { + return $this->count(); + } + + public function toArray($deep = true, $prefixKey = false) + { + if ($this->_state == self::STATE_LOCKED) { + return false; + } + + $stateBeforeLock = $this->_state; + $this->_state = self::STATE_LOCKED; + + $a = array(); + + foreach ($this as $column => $value) { + if ($value === self::$_null || is_object($value)) { + $value = null; + } + + $a[$column] = $value; + } + + if ($this->_table->getIdentifierType() == IPF_ORM::IDENTIFIER_AUTOINC) { + $i = $this->_table->getIdentifier(); + $a[$i] = $this->getIncremented(); + } + + if ($deep) { + foreach ($this->_references as $key => $relation) { + if (! $relation instanceof IPF_ORM_Null) { + $a[$key] = $relation->toArray($deep, $prefixKey); + } + } + } + + // [FIX] Prevent mapped IPF_ORM_Records from being displayed fully + foreach ($this->_values as $key => $value) { + if ($value instanceof IPF_ORM_Record) { + $a[$key] = $value->toArray($deep, $prefixKey); + } else { + $a[$key] = $value; + } + } + + $this->_state = $stateBeforeLock; + + return $a; + } + + public function merge($data, $deep = true) + { + if ($data instanceof $this) { + $array = $data->toArray($deep); + } else if (is_array($data)) { + $array = $data; + } + + return $this->fromArray($array, $deep); + } + + public function fromArray(array $array, $deep = true) + { + $refresh = false; + foreach ($array as $key => $value) { + if ($key == '_identifier') { + $refresh = true; + $this->assignIdentifier((array) $value); + continue; + } + + if ($deep && $this->getTable()->hasRelation($key)) { + $this->$key->fromArray($value, $deep); + } else if ($this->getTable()->hasField($key)) { + $this->set($key, $value); + } + } + + if ($refresh) { + $this->refresh(); + } + } + + public function synchronizeWithArray(array $array, $deep = true) + { + $refresh = false; + foreach ($array as $key => $value) { + if ($key == '_identifier') { + $refresh = true; + $this->assignIdentifier((array) $value); + continue; + } + + if ($deep && $this->getTable()->hasRelation($key)) { + $this->get($key)->synchronizeWithArray($value); + } else if ($this->getTable()->hasField($key)) { + $this->set($key, $value); + } + } + + // eliminate relationships missing in the $array + foreach ($this->_references as $name => $obj) { + if ( ! isset($array[$name])) { + unset($this->$name); + } + } + + if ($refresh) { + $this->refresh(); + } + } + + public function exportTo($type, $deep = true) + { + if ($type == 'array') { + return $this->toArray($deep); + } else { + return IPF_ORM_Parser::dump($this->toArray($deep, true), $type); + } + } + + public function importFrom($type, $data) + { + if ($type == 'array') { + return $this->fromArray($data); + } else { + return $this->fromArray(IPF_ORM_Parser::load($data, $type)); + } + } + + public function exists() + { + return ($this->_state !== IPF_ORM_Record::STATE_TCLEAN && + $this->_state !== IPF_ORM_Record::STATE_TDIRTY); + } + + public function isModified() + { + return ($this->_state === IPF_ORM_Record::STATE_DIRTY || + $this->_state === IPF_ORM_Record::STATE_TDIRTY); + } + + public function hasRelation($fieldName) + { + if (isset($this->_data[$fieldName]) || isset($this->_id[$fieldName])) { + return true; + } + return $this->_table->hasRelation($fieldName); + } + + public function getIterator() + { + return new IPF_ORM_Record_Iterator($this); + } + + public function delete(IPF_ORM_Connection $conn = null) + { + if ($conn == null) { + $conn = $this->_table->getConnection(); + } + return $conn->unitOfWork->delete($this); + } + + public function copy($deep = false) + { + $data = $this->_data; + + if ($this->_table->getIdentifierType() === IPF_ORM::IDENTIFIER_AUTOINC) { + $id = $this->_table->getIdentifier(); + + unset($data[$id]); + } + + $ret = $this->_table->create($data); + $modified = array(); + + foreach ($data as $key => $val) { + if ( ! ($val instanceof IPF_ORM_Null)) { + $ret->_modified[] = $key; + } + } + + if ($deep) { + foreach ($this->_references as $key => $value) { + if ($value instanceof IPF_ORM_Collection) { + foreach ($value as $record) { + $ret->{$key}[] = $record->copy($deep); + } + } else if($value instanceof IPF_ORM_Record) { + $ret->set($key, $value->copy($deep)); + } + } + } + + return $ret; + } + + public function assignIdentifier($id = false) + { + if ($id === false) { + $this->_id = array(); + $this->_data = $this->cleanData($this->_data); + $this->_state = IPF_ORM_Record::STATE_TCLEAN; + $this->_modified = array(); + } elseif ($id === true) { + $this->prepareIdentifiers(true); + $this->_state = IPF_ORM_Record::STATE_CLEAN; + $this->_modified = array(); + } else { + if (is_array($id)) { + foreach ($id as $fieldName => $value) { + $this->_id[$fieldName] = $value; + $this->_data[$fieldName] = $value; + } + } else { + $name = $this->_table->getIdentifier(); + $this->_id[$name] = $id; + $this->_data[$name] = $id; + } + $this->_state = IPF_ORM_Record::STATE_CLEAN; + $this->_modified = array(); + } + } + + public function identifier() + { + return $this->_id; + } + + final public function getIncremented() + { + $id = current($this->_id); + if ($id === false) { + return null; + } + + return $id; + } + + public function getLast() + { + return $this; + } + + public function hasReference($name) + { + return isset($this->_references[$name]); + } + + public function reference($name) + { + if (isset($this->_references[$name])) { + return $this->_references[$name]; + } + } + + public function obtainReference($name) + { + if (isset($this->_references[$name])) { + return $this->_references[$name]; + } + throw new IPF_ORM_Exception("Unknown reference $name"); + } + + public function getReferences() + { + return $this->_references; + } + + final public function setRelated($alias, IPF_ORM_Access $coll) + { + $this->_references[$alias] = $coll; + } + + public function loadReference($name) + { + $rel = $this->_table->getRelation($name); + $this->_references[$name] = $rel->fetchRelatedFor($this); + } + + public function call($callback, $column) + { + $args = func_get_args(); + array_shift($args); + + if (isset($args[0])) { + $fieldName = $args[0]; + $args[0] = $this->get($fieldName); + + $newvalue = call_user_func_array($callback, $args); + + $this->_data[$fieldName] = $newvalue; + } + return $this; + } + + public function getNode() + { + if ( ! $this->_table->isTree()) { + return false; + } + + if ( ! isset($this->_node)) { + $this->_node = IPF_ORM_Node::factory($this, + $this->getTable()->getOption('treeImpl'), + $this->getTable()->getOption('treeOptions') + ); + } + + return $this->_node; + } + + public function unshiftFilter(IPF_ORM_Record_Filter $filter) + { + return $this->_table->unshiftFilter($filter); + } + + public function unlink($alias, $ids = array()) + { + $ids = (array) $ids; + + $q = new IPF_ORM_Query(); + + $rel = $this->getTable()->getRelation($alias); + + if ($rel instanceof IPF_ORM_Relation_Association) { + $q->delete() + ->from($rel->getAssociationTable()->getComponentName()) + ->where($rel->getLocal() . ' = ?', array_values($this->identifier())); + + if (count($ids) > 0) { + $q->whereIn($rel->getForeign(), $ids); + } + + $q->execute(); + + } else if ($rel instanceof IPF_ORM_Relation_ForeignKey) { + $q->update($rel->getTable()->getComponentName()) + ->set($rel->getForeign(), '?', array(null)) + ->addWhere($rel->getForeign() . ' = ?', array_values($this->identifier())); + + if (count($ids) > 0) { + $q->whereIn($rel->getTable()->getIdentifier(), $ids); + } + + $q->execute(); + } + if (isset($this->_references[$alias])) { + foreach ($this->_references[$alias] as $k => $record) { + if (in_array(current($record->identifier()), $ids)) { + $this->_references[$alias]->remove($k); + } + } + $this->_references[$alias]->takeSnapshot(); + } + return $this; + } + + public function link($alias, $ids) + { + $ids = (array) $ids; + + if ( ! count($ids)) { + return $this; + } + + $identifier = array_values($this->identifier()); + $identifier = array_shift($identifier); + + $rel = $this->getTable()->getRelation($alias); + + if ($rel instanceof IPF_ORM_Relation_Association) { + + $modelClassName = $rel->getAssociationTable()->getComponentName(); + $localFieldName = $rel->getLocalFieldName(); + $localFieldDef = $rel->getAssociationTable()->getColumnDefinition($localFieldName); + if ($localFieldDef['type'] == 'integer') { + $identifier = (integer) $identifier; + } + $foreignFieldName = $rel->getForeignFieldName(); + $foreignFieldDef = $rel->getAssociationTable()->getColumnDefinition($foreignFieldName); + if ($foreignFieldDef['type'] == 'integer') { + for ($i = 0; $i < count($ids); $i++) { + $ids[$i] = (integer) $ids[$i]; + } + } + foreach ($ids as $id) { + $record = new $modelClassName; + $record[$localFieldName] = $identifier; + $record[$foreignFieldName] = $id; + $record->save(); + } + + } else if ($rel instanceof IPF_ORM_Relation_ForeignKey) { + + $q = new IPF_ORM_Query(); + + $q->update($rel->getTable()->getComponentName()) + ->set($rel->getForeign(), '?', array_values($this->identifier())); + + if (count($ids) > 0) { + $q->whereIn($rel->getTable()->getIdentifier(), $ids); + } + + $q->execute(); + + } else if ($rel instanceof IPF_ORM_Relation_LocalKey) { + + $q = new IPF_ORM_Query(); + + $q->update($this->getTable()->getComponentName()) + ->set($rel->getLocalFieldName(), '?', $ids); + + if (count($ids) > 0) { + $q->whereIn($rel->getTable()->getIdentifier(), array_values($this->identifier())); + } + + $q->execute(); + + } + + return $this; + } + + public function __call($method, $args) + { + if (($template = $this->_table->getMethodOwner($method)) !== false) { + $template->setInvoker($this); + return call_user_func_array(array($template, $method), $args); + } + + foreach ($this->_table->getTemplates() as $template) { + if (method_exists($template, $method)) { + $template->setInvoker($this); + $this->_table->setMethodOwner($method, $template); + + return call_user_func_array(array($template, $method), $args); + } + } + + throw new IPF_ORM_Exception(sprintf('Unknown method %s::%s', get_class($this), $method)); + } + + public function deleteNode() + { + $this->getNode()->delete(); + } + + public function free($deep = false) + { + if ($this->_state != self::STATE_LOCKED) { + $this->_state = self::STATE_LOCKED; + + $this->_table->getRepository()->evict($this->_oid); + $this->_table->removeRecord($this); + $this->_data = array(); + $this->_id = array(); + + if ($deep) { + foreach ($this->_references as $name => $reference) { + if ( ! ($reference instanceof IPF_ORM_Null)) { + $reference->free($deep); + } + } + } + + $this->_references = array(); + } + } + + public function toString() + { + return IPF_ORM::dump(get_object_vars($this)); + } + + public function __toString() + { + return (string) $this->_oid; + } + + public function ModelAdmin(){ + $cn = get_class($this); + if (isset(IPF_Admin_Model::$models[$cn])) + return IPF_Admin_Model::$models[$cn]; + return null; + } + + function SetFromFormData($cleaned_values) + { + foreach ($cleaned_values as $key=>$val) { + $this->$key = $val; + } + } +} \ No newline at end of file diff --git a/ipf/orm/record/abstract.php b/ipf/orm/record/abstract.php new file mode 100644 index 0000000..ffbe2c2 --- /dev/null +++ b/ipf/orm/record/abstract.php @@ -0,0 +1,192 @@ +_table; + } + + public function addListener($listener, $name = null) + { + $this->_table->addRecordListener($listener, $name = null); + return $this; + } + + public function getListener() + { + return $this->_table->getRecordListener(); + } + + public function setListener($listener) + { + $this->_table->setRecordListener($listener); + return $this; + } + + public function index($name, array $definition = array()) + { + if ( ! $definition) { + return $this->_table->getIndex($name); + } else { + return $this->_table->addIndex($name, $definition); + } + } + public function setAttribute($attr, $value) + { + $this->_table->setAttribute($attr, $value); + } + public function setTableName($tableName) + { + $this->_table->setTableName($tableName); + } + public function setInheritanceMap($map) + { + $this->_table->setOption('inheritanceMap', $map); + } + + public function setSubclasses($map) + { + if (isset($map[get_class($this)])) { + $this->_table->setOption('inheritanceMap', $map[get_class($this)]); + return; + } + $this->_table->setOption('subclasses', array_keys($map)); + } + + public function attribute($attr, $value) + { + if ($value == null) { + if (is_array($attr)) { + foreach ($attr as $k => $v) { + $this->_table->setAttribute($k, $v); + } + } else { + return $this->_table->getAttribute($attr); + } + } else { + $this->_table->setAttribute($attr, $value); + } + } + + public function option($name, $value = null) + { + if ($value === null) { + if (is_array($name)) { + foreach ($name as $k => $v) { + $this->_table->setOption($k, $v); + } + } else { + return $this->_table->getOption($name); + } + } else { + $this->_table->setOption($name, $value); + } + } + + public function ownsOne() + { + $this->_table->bind(func_get_args(), IPF_ORM_Relation::ONE_COMPOSITE); + + return $this; + } + + public function ownsMany() + { + $this->_table->bind(func_get_args(), IPF_ORM_Relation::MANY_COMPOSITE); + return $this; + } + + public function hasOne() + { + $this->_table->bind(func_get_args(), IPF_ORM_Relation::ONE_AGGREGATE); + return $this; + } + + public function hasMany() + { + $this->_table->bind(func_get_args(), IPF_ORM_Relation::MANY_AGGREGATE); + return $this; + } + + public function hasColumn($name, $type, $length = 2147483647, $options = "") + { + $this->_table->setColumn($name, $type, $length, $options); + } + + public function hasColumns(array $definitions) + { + foreach ($definitions as $name => $options) { + $this->hasColumn($name, $options['type'], $options['length'], $options); + } + } + + public function loadTemplate($template, array $options = array()) + { + $this->actAs($template, $options); + } + + public function bindQueryParts(array $queryParts) + { + $this->_table->bindQueryParts($queryParts); + return $this; + } + + public function loadGenerator(IPF_ORM_Record_Generator $generator) + { + $generator->initialize($this->_table); + $this->_table->addGenerator($generator, get_class($generator)); + } + + public function actAs($tpl, array $options = array()) + { + if ( ! is_object($tpl)) { + $className = 'IPF_ORM_Template_' . $tpl; + + if (class_exists($className, true)) { + $tpl = new $className($options); + } else if (class_exists($tpl, true)) { + $tpl = new $tpl($options); + } else { + throw new IPF_ORM_Record_Exception('Could not load behavior named: "' . $tpl . '"'); + } + } + + if ( ! ($tpl instanceof IPF_ORM_Template)) { + throw new IPF_ORM_Record_Exception('Loaded behavior class is not an istance of IPF_ORM_Template.'); + } + + $className = get_class($tpl); + + $this->_table->addTemplate($className, $tpl); + + $tpl->setTable($this->_table); + $tpl->setUp(); + $tpl->setTableDefinition(); + + return $this; + } + + public function check($constraint, $name = null) + { + if (is_array($constraint)) { + foreach ($constraint as $name => $def) { + $this->_table->addCheckConstraint($def, $name); + } + } else { + $this->_table->addCheckConstraint($constraint, $name); + } + return $this; + } +} diff --git a/ipf/orm/record/filter.php b/ipf/orm/record/filter.php new file mode 100644 index 0000000..ee65088 --- /dev/null +++ b/ipf/orm/record/filter.php @@ -0,0 +1,19 @@ +_table = $table; + } + + public function getTable() + { + return $this->_table; + } + + abstract public function filterSet(IPF_ORM_Record $record, $name, $value); + abstract public function filterGet(IPF_ORM_Record $record, $name); +} \ No newline at end of file diff --git a/ipf/orm/record/filter/compound.php b/ipf/orm/record/filter/compound.php new file mode 100644 index 0000000..289136d --- /dev/null +++ b/ipf/orm/record/filter/compound.php @@ -0,0 +1,58 @@ +_aliases = $aliases; + } + public function init() + { + // check that all aliases exist + foreach ($this->_aliases as $alias) { + $this->_table->getRelation($alias); + } + } + + public function filterSet(IPF_ORM_Record $record, $name, $value) + { + foreach ($this->_aliases as $alias) { + if ( ! $record->exists()) { + if (isset($record[$alias][$name])) { + $record[$alias][$name] = $value; + + return $record; + } + } else { + // we do not want to execute N + 1 queries here, hence we cannot use get() + if (($ref = $record->reference($alias)) !== null) { + if (isset($ref[$name])) { + $ref[$name] = $value; + } + + return $record; + } + } + } + } + + public function filterGet(IPF_ORM_Record $record, $name) + { + foreach ($this->_aliases as $alias) { + if ( ! $record->exists()) { + if (isset($record[$alias][$name])) { + return $record[$alias][$name]; + } + } else { + // we do not want to execute N + 1 queries here, hence we cannot use get() + if (($ref = $record->reference($alias)) !== null) { + if (isset($ref[$name])) { + return $ref[$name]; + } + } + } + } + } +} \ No newline at end of file diff --git a/ipf/orm/record/filter/standard.php b/ipf/orm/record/filter/standard.php new file mode 100644 index 0000000..d76c5c6 --- /dev/null +++ b/ipf/orm/record/filter/standard.php @@ -0,0 +1,14 @@ +record = $record; + parent::__construct($record->getData()); + } + + public static function initNullObject(IPF_ORM_Null $null) + { + self::$null = $null; + } + + public function current() + { + $value = parent::current(); + + if ($value === self::$null) { + return null; + } else { + return $value; + } + } +} \ No newline at end of file diff --git a/ipf/orm/record/listener.php b/ipf/orm/record/listener.php new file mode 100644 index 0000000..03a31e7 --- /dev/null +++ b/ipf/orm/record/listener.php @@ -0,0 +1,22 @@ +_listeners[] = $listener; + } else { + $this->_listeners[$name] = $listener; + } + } + + public function get($key) + { + if ( ! isset($this->_listeners[$key])) { + return null; + } + return $this->_listeners[$key]; + } + + public function set($key, $listener) + { + $this->_listeners[$key] = $listener; + } + + public function preSerialize(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preSerialize($event); + } + } + + public function postSerialize(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preSerialize($event); + } + } + + public function preUnserialize(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preUnserialize($event); + } + } + + public function postUnserialize(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->postUnserialize($event); + } + } + + public function preDqlSelect(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preDqlSelect($event); + } + } + + public function preSave(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preSave($event); + } + } + + public function postSave(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->postSave($event); + } + } + + public function preDqlDelete(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preDqlDelete($event); + } + } + + public function preDelete(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preDelete($event); + } + } + + public function postDelete(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->postDelete($event); + } + } + + public function preDqlUpdate(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preDqlUpdate($event); + } + } + + public function preUpdate(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preUpdate($event); + } + } + + public function postUpdate(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->postUpdate($event); + } + } + + public function preInsert(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preInsert($event); + } + } + + public function postInsert(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->postInsert($event); + } + } + + public function preHydrate(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preHydrate($event); + } + } + + public function postHydrate(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->postHydrate($event); + } + } +} diff --git a/ipf/orm/record/listener/interface.php b/ipf/orm/record/listener/interface.php new file mode 100644 index 0000000..d3e98aa --- /dev/null +++ b/ipf/orm/record/listener/interface.php @@ -0,0 +1,19 @@ + true, + 'foreign' => true, + 'local' => true, + 'class' => true, + 'type' => true, + 'table' => true, + 'localTable' => true, + 'name' => null, + 'refTable' => null, + 'onDelete' => null, + 'onUpdate' => null, + 'deferred' => null, + 'deferrable' => null, + 'constraint' => null, + 'equal' => false, + 'cascade' => array(), // application-level cascades + 'owningSide' => false, // whether this is the owning side + ); + + public function __construct(array $definition) + { + $def = array(); + foreach ($this->definition as $key => $val) { + if ( ! isset($definition[$key]) && $val) { + throw new IPF_ORM_Exception($key . ' is required!'); + } + if (isset($definition[$key])) { + $def[$key] = $definition[$key]; + } else { + $def[$key] = $this->definition[$key]; + } + } + $this->definition = $def; + } + + public function hasConstraint() + { + return ($this->definition['constraint'] || + ($this->definition['onUpdate']) || + ($this->definition['onDelete'])); + } + public function isDeferred() + { + return $this->definition['deferred']; + } + + public function isDeferrable() + { + return $this->definition['deferrable']; + } + public function isEqual() + { + return $this->definition['equal']; + } + + public function offsetExists($offset) + { + return isset($this->definition[$offset]); + } + + public function offsetGet($offset) + { + if (isset($this->definition[$offset])) { + return $this->definition[$offset]; + } + + return null; + } + + public function offsetSet($offset, $value) + { + if (isset($this->definition[$offset])) { + $this->definition[$offset] = $value; + } + } + + public function offsetUnset($offset) + { + $this->definition[$offset] = false; + } + + public function toArray() + { + return $this->definition; + } + + final public function getAlias() + { + return $this->definition['alias']; + } + + final public function getType() + { + return $this->definition['type']; + } + + public function isCascadeDelete() + { + return in_array('delete', $this->definition['cascade']); + } + + final public function getTable() + { + return IPF_ORM_Manager::getInstance() + ->getConnectionForComponent($this->definition['class']) + ->getTable($this->definition['class']); + } + + final public function getClass() + { + return $this->definition['class']; + } + + final public function getLocal() + { + return $this->definition['local']; + } + + final public function getLocalFieldName() + { + return $this->definition['localTable']->getFieldName($this->definition['local']); + } + + final public function getForeign() + { + return $this->definition['foreign']; + } + + final public function getForeignFieldName() + { + return $this->definition['table']->getFieldName($this->definition['foreign']); + } + + final public function isComposite() + { + return ($this->definition['type'] == IPF_ORM_Relation::ONE_COMPOSITE || + $this->definition['type'] == IPF_ORM_Relation::MANY_COMPOSITE); + } + + final public function isOneToOne() + { + return ($this->definition['type'] == IPF_ORM_Relation::ONE_AGGREGATE || + $this->definition['type'] == IPF_ORM_Relation::ONE_COMPOSITE); + } + + public function getRelationDql($count) + { + $component = $this->getTable()->getComponentName(); + + $dql = 'FROM ' . $component + . ' WHERE ' . $component . '.' . $this->definition['foreign'] + . ' IN (' . substr(str_repeat('?, ', $count), 0, -2) . ')'; + + return $dql; + } + + abstract public function fetchRelatedFor(IPF_ORM_Record $record); + + public function __toString() + { + $r[] = "
    ";
    +        foreach ($this->definition as $k => $v) {
    +            if (is_object($v)) {
    +                $v = 'Object(' . get_class($v) . ')';
    +            }
    +            $r[] = $k . ' : ' . $v;
    +        }
    +        $r[] = "
    "; + return implode("\n", $r); + } +} \ No newline at end of file diff --git a/ipf/orm/relation/association.php b/ipf/orm/relation/association.php new file mode 100644 index 0000000..4a1958d --- /dev/null +++ b/ipf/orm/relation/association.php @@ -0,0 +1,48 @@ +definition['refTable']; + } + public function getAssociationTable() + { + return $this->definition['refTable']; + } + + public function getRelationDql($count, $context = 'record') + { + $table = $this->definition['refTable']; + $component = $this->definition['refTable']->getComponentName(); + + switch ($context) { + case "record": + $sub = substr(str_repeat("?, ", $count),0,-2); + $dql = 'FROM ' . $this->getTable()->getComponentName(); + $dql .= '.' . $component; + $dql .= ' WHERE ' . $this->getTable()->getComponentName() + . '.' . $component . '.' . $this->definition['local'] . ' IN (' . $sub . ')'; + break; + case "collection": + $sub = substr(str_repeat("?, ", $count),0,-2); + $dql = 'FROM ' . $component . '.' . $this->getTable()->getComponentName(); + $dql .= ' WHERE ' . $component . '.' . $this->definition['local'] . ' IN (' . $sub . ')'; + break; + } + + return $dql; + } + + public function fetchRelatedFor(IPF_ORM_Record $record) + { + $id = $record->getIncremented(); + if (empty($id) || ! $this->definition['table']->getAttribute(IPF_ORM::ATTR_LOAD_REFERENCES)) { + $coll = new IPF_ORM_Collection($this->getTable()); + } else { + $coll = $this->getTable()->getConnection()->query($this->getRelationDql(1), array($id)); + } + $coll->setReference($record, $this); + return $coll; + } +} \ No newline at end of file diff --git a/ipf/orm/relation/foreignkey.php b/ipf/orm/relation/foreignkey.php new file mode 100644 index 0000000..8830a4b --- /dev/null +++ b/ipf/orm/relation/foreignkey.php @@ -0,0 +1,56 @@ +getTable(); + foreach ((array) $this->definition['local'] as $local) { + $value = $record->get($localTable->getFieldName($local)); + if (isset($value)) { + $id[] = $value; + } + } + if ($this->isOneToOne()) { + if ( ! $record->exists() || empty($id) || + ! $this->definition['table']->getAttribute(IPF_ORM::ATTR_LOAD_REFERENCES)) { + + $related = $this->getTable()->create(); + } else { + $dql = 'FROM ' . $this->getTable()->getComponentName() + . ' WHERE ' . $this->getCondition(); + + $coll = $this->getTable()->getConnection()->query($dql, $id); + $related = $coll[0]; + } + + $related->set($related->getTable()->getFieldName($this->definition['foreign']), + $record, false); + } else { + + if ( ! $record->exists() || empty($id) || + ! $this->definition['table']->getAttribute(IPF_ORM::ATTR_LOAD_REFERENCES)) { + + $related = new IPF_ORM_Collection($this->getTable()); + } else { + $query = $this->getRelationDql(1); + $related = $this->getTable()->getConnection()->query($query, $id); + } + $related->setReference($record, $this); + } + return $related; + } + + public function getCondition($alias = null) + { + if ( ! $alias) { + $alias = $this->getTable()->getComponentName(); + } + $conditions = array(); + foreach ((array) $this->definition['foreign'] as $foreign) { + $conditions[] = $alias . '.' . $foreign . ' = ?'; + } + return implode(' AND ', $conditions); + } +} \ No newline at end of file diff --git a/ipf/orm/relation/localkey.php b/ipf/orm/relation/localkey.php new file mode 100644 index 0000000..ebe8e66 --- /dev/null +++ b/ipf/orm/relation/localkey.php @@ -0,0 +1,38 @@ +getTable()->getFieldName($this->definition['local']); + $id = $record->get($localFieldName); + + if (is_null($id) || ! $this->definition['table']->getAttribute(IPF_ORM::ATTR_LOAD_REFERENCES)) { + $related = $this->getTable()->create(); + } else { + $dql = 'FROM ' . $this->getTable()->getComponentName() + . ' WHERE ' . $this->getCondition(); + + $related = $this->getTable() + ->getConnection() + ->query($dql, array($id)) + ->getFirst(); + + if ( ! $related || empty($related)) { + $related = $this->getTable()->create(); + } + } + + $record->set($localFieldName, $related, false); + + return $related; + } + + public function getCondition($alias = null) + { + if ( ! $alias) { + $alias = $this->getTable()->getComponentName(); + } + return $alias . '.' . $this->definition['foreign'] . ' = ?'; + } +} \ No newline at end of file diff --git a/ipf/orm/relation/nest.php b/ipf/orm/relation/nest.php new file mode 100644 index 0000000..f23f90a --- /dev/null +++ b/ipf/orm/relation/nest.php @@ -0,0 +1,84 @@ +definition['table']->getIdentifierColumnNames(); + $identifier = array_pop($identifierColumnNames); + $sub = 'SELECT '.$this->definition['foreign'] + . ' FROM '.$this->definition['refTable']->getTableName() + . ' WHERE '.$this->definition['local'] + . ' = ?'; + + $sub2 = 'SELECT '.$this->definition['local'] + . ' FROM '.$this->definition['refTable']->getTableName() + . ' WHERE '.$this->definition['foreign'] + . ' = ?'; + + $dql = 'FROM ' . $this->definition['table']->getComponentName() + . '.' . $this->definition['refTable']->getComponentName() + . ' WHERE ' . $this->definition['table']->getComponentName() + . '.' . $identifier + . ' IN (' . $sub . ')' + . ' || ' . $this->definition['table']->getComponentName() + . '.' . $identifier + . ' IN (' . $sub2 . ')'; + break; + case 'collection': + $sub = substr(str_repeat('?, ', $count),0,-2); + $dql = 'FROM '.$this->definition['refTable']->getComponentName() + . '.' . $this->definition['table']->getComponentName() + . ' WHERE '.$this->definition['refTable']->getComponentName() + . '.' . $this->definition['local'] . ' IN (' . $sub . ')'; + }; + + return $dql; + } + + public function fetchRelatedFor(IPF_ORM_Record $record) + { + $id = $record->getIncremented(); + + + if (empty($id) || ! $this->definition['table']->getAttribute(IPF_ORM::ATTR_LOAD_REFERENCES)) { + return new IPF_ORM_Collection($this->getTable()); + } else { + $q = new IPF_ORM_RawSql($this->getTable()->getConnection()); + + $assocTable = $this->getAssociationFactory()->getTableName(); + $tableName = $record->getTable()->getTableName(); + $identifierColumnNames = $record->getTable()->getIdentifierColumnNames(); + $identifier = array_pop($identifierColumnNames); + + $sub = 'SELECT ' . $this->getForeign() + . ' FROM ' . $assocTable + . ' WHERE ' . $this->getLocal() + . ' = ?'; + + $condition[] = $tableName . '.' . $identifier . ' IN (' . $sub . ')'; + $joinCondition[] = $tableName . '.' . $identifier . ' = ' . $assocTable . '.' . $this->getForeign(); + + if ($this->definition['equal']) { + $sub2 = 'SELECT ' . $this->getLocal() + . ' FROM ' . $assocTable + . ' WHERE ' . $this->getForeign() + . ' = ?'; + + $condition[] = $tableName . '.' . $identifier . ' IN (' . $sub2 . ')'; + $joinCondition[] = $tableName . '.' . $identifier . ' = ' . $assocTable . '.' . $this->getLocal(); + } + $q->select('{'.$tableName.'.*}, {'.$assocTable.'.*}') + ->from($tableName . ' INNER JOIN ' . $assocTable . ' ON ' . implode(' OR ', $joinCondition)) + ->where(implode(' OR ', $condition)); + $q->addComponent($tableName, $record->getTable()->getComponentName()); + $q->addComponent($assocTable, $record->getTable()->getComponentName(). '.' . $this->getAssociationFactory()->getComponentName()); + + $params = ($this->definition['equal']) ? array($id, $id) : array($id); + + return $q->execute($params); + } + } +} \ No newline at end of file diff --git a/ipf/orm/relation/parser.php b/ipf/orm/relation/parser.php new file mode 100644 index 0000000..e15228e --- /dev/null +++ b/ipf/orm/relation/parser.php @@ -0,0 +1,390 @@ +_table = $table; + } + + public function getTable() + { + return $this->_table; + } + + public function getPendingRelation($name) + { + if ( ! isset($this->_pending[$name])) { + throw new IPF_ORM_Exception('Unknown pending relation ' . $name); + } + + return $this->_pending[$name]; + } + + public function getPendingRelations() + { + return $this->_pending; + } + + public function unsetPendingRelations($name) + { + unset($this->_pending[$name]); + } + + public function hasRelation($name) + { + if ( ! isset($this->_pending[$name]) && ! isset($this->_relations[$name])) { + return false; + } + + return true; + } + + public function bind($name, $options = array()) + { + $e = explode(' as ', $name); + $name = $e[0]; + $alias = isset($e[1]) ? $e[1] : $name; + + if ( ! isset($options['type'])) { + throw new IPF_ORM_Exception('Relation type not set.'); + } + + if ($this->hasRelation($alias)) { + unset($this->relations[$alias]); + unset($this->_pending[$alias]); + } + + $this->_pending[$alias] = array_merge($options, array('class' => $name, 'alias' => $alias)); + + return $this->_pending[$alias]; + } + + public function getRelation($alias, $recursive = true) + { + if (isset($this->_relations[$alias])) { + return $this->_relations[$alias]; + } + + if (isset($this->_pending[$alias])) { + $def = $this->_pending[$alias]; + $identifierColumnNames = $this->_table->getIdentifierColumnNames(); + $idColumnName = array_pop($identifierColumnNames); + + // check if reference class name exists + // if it does we are dealing with association relation + if (isset($def['refClass'])) { + $def = $this->completeAssocDefinition($def); + $localClasses = array_merge($this->_table->getOption('parents'), array($this->_table->getComponentName())); + + if ( ! isset($this->_pending[$def['refClass']]) && + ! isset($this->_relations[$def['refClass']])) { + + $parser = $def['refTable']->getRelationParser(); + if ( ! $parser->hasRelation($this->_table->getComponentName())) { + $parser->bind($this->_table->getComponentName(), + array('type' => IPF_ORM_Relation::ONE, + 'local' => $def['local'], + 'foreign' => $idColumnName, + 'localKey' => true, + )); + } + + if ( ! $this->hasRelation($def['refClass'])) { + $this->bind($def['refClass'], array('type' => IPF_ORM_Relation::MANY, + 'foreign' => $def['local'], + 'local' => $idColumnName)); + } + } + if (in_array($def['class'], $localClasses)) { + $rel = new IPF_ORM_Relation_Nest($def); + } else { + $rel = new IPF_ORM_Relation_Association($def); + } + } else { + // simple foreign key relation + $def = $this->completeDefinition($def); + + if (isset($def['localKey'])) { + $rel = new IPF_ORM_Relation_LocalKey($def); + + // Automatically index foreign keys which are not primary + $foreign = (array) $def['foreign']; + foreach ($foreign as $fk) { + if ( ! $rel->getTable()->isIdentifier($fk)) { + $rel->getTable()->addIndex($fk, array('fields' => array($fk))); + } + } + } else { + $rel = new IPF_ORM_Relation_ForeignKey($def); + } + } + if (isset($rel)) { + // unset pending relation + unset($this->_pending[$alias]); + + $this->_relations[$alias] = $rel; + return $rel; + } + } + if ($recursive) { + $this->getRelations(); + + return $this->getRelation($alias, false); + } else { + throw new IPF_ORM_Exception('Unknown relation alias ' . $alias); + } + } + + public function getRelations() + { + foreach ($this->_pending as $k => $v) { + $this->getRelation($k); + } + + return $this->_relations; + } + + public function getImpl($template) + { + $conn = $this->_table->getConnection(); + + if (in_array('IPF_ORM_Template', class_parents($template))) { + $impl = $this->_table->getImpl($template); + + if ($impl === null) { + throw new IPF_ORM_Exception("Couldn't find concrete implementation for template " . $template); + } + } else { + $impl = $template; + } + + return $conn->getTable($impl); + } + + public function completeAssocDefinition($def) + { + $conn = $this->_table->getConnection(); + $def['table'] = $this->getImpl($def['class']); + $def['localTable'] = $this->_table; + $def['class'] = $def['table']->getComponentName(); + $def['refTable'] = $this->getImpl($def['refClass']); + + $id = $def['refTable']->getIdentifierColumnNames(); + + if (count($id) > 1) { + if ( ! isset($def['foreign'])) { + // foreign key not set + // try to guess the foreign key + + $def['foreign'] = ($def['local'] === $id[0]) ? $id[1] : $id[0]; + } + if ( ! isset($def['local'])) { + // foreign key not set + // try to guess the foreign key + + $def['local'] = ($def['foreign'] === $id[0]) ? $id[1] : $id[0]; + } + } else { + + if ( ! isset($def['foreign'])) { + // foreign key not set + // try to guess the foreign key + + $columns = $this->getIdentifiers($def['table']); + + $def['foreign'] = $columns; + } + if ( ! isset($def['local'])) { + // local key not set + // try to guess the local key + $columns = $this->getIdentifiers($this->_table); + + $def['local'] = $columns; + } + } + return $def; + } + + public function getIdentifiers(IPF_ORM_Table $table) + { + $componentNameToLower = strtolower($table->getComponentName()); + if (is_array($table->getIdentifier())) { + $columns = array(); + foreach ((array) $table->getIdentifierColumnNames() as $identColName) { + $columns[] = $componentNameToLower . '_' . $identColName; + } + } else { + $columns = $componentNameToLower . '_' . $table->getColumnName( + $table->getIdentifier()); + } + + return $columns; + } + + public function guessColumns(array $classes, IPF_ORM_Table $foreignTable) + { + $conn = $this->_table->getConnection(); + + foreach ($classes as $class) { + try { + $table = $conn->getTable($class); + } catch (IPF_ORM_Exception $e) { + continue; + } + $columns = $this->getIdentifiers($table); + $found = true; + + foreach ((array) $columns as $column) { + if ( ! $foreignTable->hasColumn($column)) { + $found = false; + break; + } + } + if ($found) { + break; + } + } + + if ( ! $found) { + throw new IPF_ORM_Exception("Couldn't find columns."); + } + + return $columns; + } + + public function completeDefinition($def) + { + $conn = $this->_table->getConnection(); + $def['table'] = $this->getImpl($def['class']); + $def['localTable'] = $this->_table; + $def['class'] = $def['table']->getComponentName(); + + $foreignClasses = array_merge($def['table']->getOption('parents'), array($def['class'])); + $localClasses = array_merge($this->_table->getOption('parents'), array($this->_table->getComponentName())); + + $localIdentifierColumnNames = $this->_table->getIdentifierColumnNames(); + $localIdentifierCount = count($localIdentifierColumnNames); + $localIdColumnName = array_pop($localIdentifierColumnNames); + $foreignIdentifierColumnNames = $def['table']->getIdentifierColumnNames(); + $foreignIdColumnName = array_pop($foreignIdentifierColumnNames); + + if (isset($def['local'])) { + if ( ! isset($def['foreign'])) { + // local key is set, but foreign key is not + // try to guess the foreign key + + if ($def['local'] === $localIdColumnName) { + $def['foreign'] = $this->guessColumns($localClasses, $def['table']); + } else { + // the foreign field is likely to be the + // identifier of the foreign class + $def['foreign'] = $foreignIdColumnName; + $def['localKey'] = true; + } + } else { + if ($localIdentifierCount == 1) { + if ($def['local'] == $localIdColumnName && isset($def['owningSide']) + && $def['owningSide'] === true) { + $def['localKey'] = true; + } else if (($def['local'] !== $localIdColumnName && $def['type'] == IPF_ORM_Relation::ONE)) { + $def['localKey'] = true; + } + } else if ($localIdentifierCount > 1) { + // It's a composite key and since 'foreign' can not point to a composite + // key currently, we know that 'local' must be the foreign key. + $def['localKey'] = true; + } + } + } else { + if (isset($def['foreign'])) { + // local key not set, but foreign key is set + // try to guess the local key + if ($def['foreign'] === $foreignIdColumnName) { + $def['localKey'] = true; + try { + $def['local'] = $this->guessColumns($foreignClasses, $this->_table); + } catch (IPF_ORM_Exception $e) { + $def['local'] = $localIdColumnName; + } + } else { + $def['local'] = $localIdColumnName; + } + } else { + // neither local or foreign key is being set + // try to guess both keys + + $conn = $this->_table->getConnection(); + + // the following loops are needed for covering inheritance + foreach ($localClasses as $class) { + $table = $conn->getTable($class); + $identifierColumnNames = $table->getIdentifierColumnNames(); + $idColumnName = array_pop($identifierColumnNames); + $column = strtolower($table->getComponentName()) + . '_' . $idColumnName; + + foreach ($foreignClasses as $class2) { + $table2 = $conn->getTable($class2); + if ($table2->hasColumn($column)) { + $def['foreign'] = $column; + $def['local'] = $idColumnName; + return $def; + } + } + } + + foreach ($foreignClasses as $class) { + $table = $conn->getTable($class); + $identifierColumnNames = $table->getIdentifierColumnNames(); + $idColumnName = array_pop($identifierColumnNames); + $column = strtolower($table->getComponentName()) + . '_' . $idColumnName; + + foreach ($localClasses as $class2) { + $table2 = $conn->getTable($class2); + if ($table2->hasColumn($column)) { + $def['foreign'] = $idColumnName; + $def['local'] = $column; + $def['localKey'] = true; + return $def; + } + } + } + + // auto-add columns and auto-build relation + $columns = array(); + foreach ((array) $this->_table->getIdentifierColumnNames() as $id) { + // ?? should this not be $this->_table->getComponentName() ?? + $column = strtolower($table->getComponentName()) + . '_' . $id; + + $col = $this->_table->getColumnDefinition($id); + $type = $col['type']; + $length = $col['length']; + + unset($col['type']); + unset($col['length']); + unset($col['autoincrement']); + unset($col['sequence']); + unset($col['primary']); + + $def['table']->setColumn($column, $type, $length, $col); + + $columns[] = $column; + } + if (count($columns) > 1) { + $def['foreign'] = $columns; + } else { + $def['foreign'] = $columns[0]; + } + $def['local'] = $localIdColumnName; + } + } + return $def; + } +} \ No newline at end of file diff --git a/ipf/orm/sequence.php b/ipf/orm/sequence.php new file mode 100644 index 0000000..77ce93b --- /dev/null +++ b/ipf/orm/sequence.php @@ -0,0 +1,21 @@ +warnings[] = 'database does not support getting current + sequence value, the sequence value was incremented'; + return $this->nextId($seqName); + } +} \ No newline at end of file diff --git a/ipf/orm/sequence/mysql.php b/ipf/orm/sequence/mysql.php new file mode 100644 index 0000000..4293095 --- /dev/null +++ b/ipf/orm/sequence/mysql.php @@ -0,0 +1,52 @@ +conn->quoteIdentifier($seqName, true); + $seqcolName = $this->conn->quoteIdentifier($this->conn->getAttribute(IPF_ORM::ATTR_SEQCOL_NAME), true); + $query = 'INSERT INTO ' . $sequenceName . ' (' . $seqcolName . ') VALUES (NULL)'; + + try { + + $this->conn->exec($query); + + } catch(IPF_ORM_Exception $e) { + if ($onDemand && $e->getPortableCode() == IPF_ORM::ERR_NOSUCHTABLE) { + // Since we are creating the sequence on demand + // we know the first id = 1 so initialize the + // sequence at 2 + try { + $result = $this->conn->export->createSequence($seqName, 2); + } catch(IPF_ORM_Exception $e) { + throw new IPF_ORM_Exception('on demand sequence ' . $seqName . ' could not be created'); + } + // First ID of a newly created sequence is 1 + return 1; + } + throw $e; + } + + $value = $this->lastInsertId(); + + if (is_numeric($value)) { + $query = 'DELETE FROM ' . $sequenceName . ' WHERE ' . $seqcolName . ' < ' . $value; + $this->conn->exec($query); + } + return $value; + } + + public function lastInsertId($table = null, $field = null) + { + return $this->conn->getDbh()->lastInsertId(); + } + + public function currId($seqName) + { + $sequenceName = $this->conn->quoteIdentifier($seqName, true); + $seqcolName = $this->conn->quoteIdentifier($this->conn->getAttribute(IPF_ORM::ATTR_SEQCOL_NAME), true); + $query = 'SELECT MAX(' . $seqcolName . ') FROM ' . $sequenceName; + return (int) $this->conn->fetchOne($query); + } +} \ No newline at end of file diff --git a/ipf/orm/table.php b/ipf/orm/table.php new file mode 100644 index 0000000..05ea8d5 --- /dev/null +++ b/ipf/orm/table.php @@ -0,0 +1,1405 @@ + null, + 'tableName' => null, + 'sequenceName' => null, + 'inheritanceMap' => array(), + 'enumMap' => array(), + 'type' => null, + 'charset' => null, + 'collation' => null, + 'treeImpl' => null, + 'treeOptions' => array(), + 'indexes' => array(), + 'parents' => array(), + 'joinedParents' => array(), + 'queryParts' => array(), + 'versioning' => null, + 'subclasses' => array(), + ); + + protected $_tree; + protected $_parser; + + protected $_templates = array(); + protected $_filters = array(); + protected $_generators = array(); + protected $_invokedMethods = array(); + protected $record; + + public function __construct($name, IPF_ORM_Connection $conn, $initDefinition = false) + { + $this->_conn = $conn; + + $this->setParent($this->_conn); + + $this->_options['name'] = $name; + $this->_parser = new IPF_ORM_Relation_Parser($this); + + if ($initDefinition) { + $this->record = $this->initDefinition(); + + $this->initIdentifier(); + + $this->record->setUp(); + + // if tree, set up tree + if ($this->isTree()) { + $this->getTree()->setUp(); + } + } else { + if ( ! isset($this->_options['tableName'])) { + $this->setTableName(IPF_ORM_Inflector::tableize($this->_options['name'])); + } + } + $this->_filters[] = new IPF_ORM_Record_Filter_Standard(); + $this->_repository = new IPF_ORM_Table_Repository($this); + } + + public function initDefinition() + { + $name = $this->_options['name']; + if ( ! class_exists($name) || empty($name)) { + throw new IPF_ORM_Exception("Couldn't find class " . $name); + } + $record = new $name($this); + + $names = array(); + + $class = $name; + + // get parent classes + + do { + if ($class === 'IPF_ORM_Record') { + break; + } + + $name = $class; + $names[] = $name; + } while ($class = get_parent_class($class)); + + if ($class === false) { + throw new IPF_ORM_Exception('Class "' . $name . '" must be a child class of IPF_ORM_Record'); + } + + // reverse names + $names = array_reverse($names); + // save parents + array_pop($names); + $this->_options['parents'] = $names; + + // create database table + if (method_exists($record, 'setTableDefinition')) { + $record->setTableDefinition(); + // get the declaring class of setTableDefinition method + $method = new ReflectionMethod($this->_options['name'], 'setTableDefinition'); + $class = $method->getDeclaringClass(); + + } else { + $class = new ReflectionClass($class); + } + + $this->_options['joinedParents'] = array(); + + foreach (array_reverse($this->_options['parents']) as $parent) { + + if ($parent === $class->getName()) { + continue; + } + $ref = new ReflectionClass($parent); + + if ($ref->isAbstract()) { + continue; + } + $parentTable = $this->_conn->getTable($parent); + + $found = false; + $parentColumns = $parentTable->getColumns(); + + foreach ($parentColumns as $columnName => $definition) { + if ( ! isset($definition['primary']) || $definition['primary'] === false) { + if (isset($this->_columns[$columnName])) { + $found = true; + break; + } else { + if ( ! isset($parentColumns[$columnName]['owner'])) { + $parentColumns[$columnName]['owner'] = $parentTable->getComponentName(); + } + + $this->_options['joinedParents'][] = $parentColumns[$columnName]['owner']; + } + } else { + unset($parentColumns[$columnName]); + } + } + + if ($found) { + continue; + } + + foreach ($parentColumns as $columnName => $definition) { + $fullName = $columnName . ' as ' . $parentTable->getFieldName($columnName); + $this->setColumn($fullName, $definition['type'], $definition['length'], $definition, true); + } + + break; + } + + $this->_options['joinedParents'] = array_values(array_unique($this->_options['joinedParents'])); + + $this->_options['declaringClass'] = $class; + + // set the table definition for the given tree implementation + if ($this->isTree()) { + $this->getTree()->setTableDefinition(); + } + + $this->columnCount = count($this->_columns); + + if ( ! isset($this->_options['tableName'])) { + $this->setTableName(IPF_ORM_Inflector::tableize($class->getName())); + } + + return $record; + } + + public function initIdentifier() + { + switch (count($this->_identifier)) { + case 0: + if ( ! empty($this->_options['joinedParents'])) { + $root = current($this->_options['joinedParents']); + + $table = $this->_conn->getTable($root); + + $this->_identifier = $table->getIdentifier(); + + $this->_identifierType = ($table->getIdentifierType() !== IPF_ORM::IDENTIFIER_AUTOINC) + ? $table->getIdentifierType() : IPF_ORM::IDENTIFIER_NATURAL; + + // add all inherited primary keys + foreach ((array) $this->_identifier as $id) { + $definition = $table->getDefinitionOf($id); + + // inherited primary keys shouldn't contain autoinc + // and sequence definitions + unset($definition['autoincrement']); + unset($definition['sequence']); + + // add the inherited primary key column + $fullName = $id . ' as ' . $table->getFieldName($id); + $this->setColumn($fullName, $definition['type'], $definition['length'], + $definition, true); + } + } else { + $definition = array('type' => 'integer', + 'length' => 20, + 'autoincrement' => true, + 'primary' => true); + $this->setColumn('id', $definition['type'], $definition['length'], $definition, true); + $this->_identifier = 'id'; + $this->_identifierType = IPF_ORM::IDENTIFIER_AUTOINC; + } + $this->columnCount++; + break; + case 1: + foreach ($this->_identifier as $pk) { + $e = $this->getDefinitionOf($pk); + + $found = false; + + foreach ($e as $option => $value) { + if ($found) { + break; + } + + $e2 = explode(':', $option); + + switch (strtolower($e2[0])) { + case 'autoincrement': + case 'autoinc': + if ($value !== false) { + $this->_identifierType = IPF_ORM::IDENTIFIER_AUTOINC; + $found = true; + } + break; + case 'seq': + case 'sequence': + $this->_identifierType = IPF_ORM::IDENTIFIER_SEQUENCE; + $found = true; + + if (is_string($value)) { + $this->_options['sequenceName'] = $value; + } else { + if (($sequence = $this->getAttribute(IPF_ORM::ATTR_DEFAULT_SEQUENCE)) !== null) { + $this->_options['sequenceName'] = $sequence; + } else { + $this->_options['sequenceName'] = $this->_conn->formatter->getSequenceName($this->_options['tableName']); + } + } + break; + } + } + if ( ! isset($this->_identifierType)) { + $this->_identifierType = IPF_ORM::IDENTIFIER_NATURAL; + } + } + + $this->_identifier = $pk; + + break; + default: + $this->_identifierType = IPF_ORM::IDENTIFIER_COMPOSITE; + } + } + + public function getColumnOwner($columnName) + { + if (isset($this->_columns[$columnName]['owner'])) { + return $this->_columns[$columnName]['owner']; + } else { + return $this->getComponentName(); + } + } + + public function getRecordInstance() + { + if ( ! $this->record) { + $this->record = new $this->_options['name']; + } + return $this->record; + } + + public function isInheritedColumn($columnName) + { + return (isset($this->_columns[$columnName]['owner'])); + } + + public function isIdentifier($fieldName) + { + return ($fieldName === $this->getIdentifier() || + in_array($fieldName, (array) $this->getIdentifier())); + } + + public function isIdentifierAutoincrement() + { + return $this->getIdentifierType() === IPF_ORM::IDENTIFIER_AUTOINC; + } + + public function isIdentifierComposite() + { + return $this->getIdentifierType() === IPF_ORM::IDENTIFIER_COMPOSITE; + } + + public function getMethodOwner($method) + { + return (isset($this->_invokedMethods[$method])) ? + $this->_invokedMethods[$method] : false; + } + + public function setMethodOwner($method, $class) + { + $this->_invokedMethods[$method] = $class; + } + + public function getTemplates() + { + return $this->_templates; + } + + public function export() + { + $this->_conn->export->exportTable($this); + } + + public function getExportableFormat($parseForeignKeys = true) + { + $columns = array(); + $primary = array(); + + foreach ($this->getColumns() as $name => $definition) { + + if (isset($definition['owner'])) { + continue; + } + + switch ($definition['type']) { + case 'enum': + if (isset($definition['default'])) { + $definition['default'] = $this->enumIndex($name, $definition['default']); + } + break; + case 'boolean': + if (isset($definition['default'])) { + $definition['default'] = $this->getConnection()->convertBooleans($definition['default']); + } + break; + } + $columns[$name] = $definition; + + if (isset($definition['primary']) && $definition['primary']) { + $primary[] = $name; + } + } + + $options['foreignKeys'] = isset($this->_options['foreignKeys']) ? + $this->_options['foreignKeys'] : array(); + + if ($parseForeignKeys && $this->getAttribute(IPF_ORM::ATTR_EXPORT) + & IPF_ORM::EXPORT_CONSTRAINTS) { + + $constraints = array(); + + $emptyIntegrity = array('onUpdate' => null, + 'onDelete' => null); + + foreach ($this->getRelations() as $name => $relation) { + $fk = $relation->toArray(); + $fk['foreignTable'] = $relation->getTable()->getTableName(); + + if ($relation->getTable() === $this && in_array($relation->getLocal(), $primary)) { + if ($relation->hasConstraint()) { + throw new IPF_ORM_Exception("Badly constructed integrity constraints."); + } + continue; + } + + $integrity = array('onUpdate' => $fk['onUpdate'], + 'onDelete' => $fk['onDelete']); + + if ($relation instanceof IPF_ORM_Relation_LocalKey) { + $def = array('local' => $relation->getLocal(), + 'foreign' => $relation->getForeign(), + 'foreignTable' => $relation->getTable()->getTableName()); + + if (($key = array_search($def, $options['foreignKeys'])) === false) { + $options['foreignKeys'][] = $def; + $constraints[] = $integrity; + } else { + if ($integrity !== $emptyIntegrity) { + $constraints[$key] = $integrity; + } + } + } + } + + foreach ($constraints as $k => $def) { + $options['foreignKeys'][$k] = array_merge($options['foreignKeys'][$k], $def); + } + } + + $options['primary'] = $primary; + + return array('tableName' => $this->getOption('tableName'), + 'columns' => $columns, + 'options' => array_merge($this->getOptions(), $options)); + } + + public function getRelationParser() + { + return $this->_parser; + } + + public function __get($option) + { + if (isset($this->_options[$option])) { + return $this->_options[$option]; + } + return null; + } + + public function __isset($option) + { + return isset($this->_options[$option]); + } + + public function getOptions() + { + return $this->_options; + } + + public function setOptions($options) + { + foreach ($options as $key => $value) { + $this->setOption($key, $value); + } + } + + public function addForeignKey(array $definition) + { + $this->_options['foreignKeys'][] = $definition; + } + + public function addCheckConstraint($definition, $name) + { + if (is_string($name)) { + $this->_options['checks'][$name] = $definition; + } else { + $this->_options['checks'][] = $definition; + } + + return $this; + } + + public function addIndex($index, array $definition) + { + $this->_options['indexes'][$index] = $definition; + } + + public function getIndex($index) + { + if (isset($this->_options['indexes'][$index])) { + return $this->_options['indexes'][$index]; + } + + return false; + } + + public function bind($args, $type) + { + $options = array(); + $options['type'] = $type; + + if ( ! isset($args[1])) { + $args[1] = array(); + } + + // the following is needed for backwards compatibility + if (is_string($args[1])) { + if ( ! isset($args[2])) { + $args[2] = array(); + } elseif (is_string($args[2])) { + $args[2] = (array) $args[2]; + } + + $classes = array_merge($this->_options['parents'], array($this->getComponentName())); + + $e = explode('.', $args[1]); + if (in_array($e[0], $classes)) { + if ($options['type'] >= IPF_ORM_Relation::MANY) { + $options['foreign'] = $e[1]; + } else { + $options['local'] = $e[1]; + } + } else { + $e2 = explode(' as ', $args[0]); + if ($e[0] !== $e2[0] && ( ! isset($e2[1]) || $e[0] !== $e2[1])) { + $options['refClass'] = $e[0]; + } + + $options['foreign'] = $e[1]; + } + + $options = array_merge($args[2], $options); + } else { + $options = array_merge($args[1], $options); + } + + $this->_parser->bind($args[0], $options); + } + + public function hasRelation($alias) + { + return $this->_parser->hasRelation($alias); + } + + public function getRelation($alias, $recursive = true) + { + return $this->_parser->getRelation($alias, $recursive); + } + + public function getRelations() + { + return $this->_parser->getRelations(); + } + + public function createQuery($alias = '') + { + if ( ! empty($alias)) { + $alias = ' ' . trim($alias); + } + return IPF_ORM_Query::create($this->_conn)->from($this->getComponentName() . $alias); + } + + public function getRepository() + { + return $this->_repository; + } + + public function setOption($name, $value) + { + switch ($name) { + case 'name': + case 'tableName': + break; + case 'enumMap': + case 'inheritanceMap': + case 'index': + case 'treeOptions': + if ( ! is_array($value)) { + throw new IPF_ORM_Exception($name . ' should be an array.'); + } + break; + } + $this->_options[$name] = $value; + } + + public function getOption($name) + { + if (isset($this->_options[$name])) { + return $this->_options[$name]; + } + return null; + } + + public function getColumnName($fieldName) + { + // FIX ME: This is being used in places where an array is passed, but it should not be an array + // For example in places where IPF_ORM should support composite foreign/primary keys + $fieldName = is_array($fieldName) ? $fieldName[0]:$fieldName; + + if (isset($this->_columnNames[$fieldName])) { + return $this->_columnNames[$fieldName]; + } + + return strtolower($fieldName); + } + + public function getColumnDefinition($columnName) + { + if ( ! isset($this->_columns[$columnName])) { + return false; + } + return $this->_columns[$columnName]; + } + + public function getFieldName($columnName) + { + if (isset($this->_fieldNames[$columnName])) { + return $this->_fieldNames[$columnName]; + } + return $columnName; + } + public function setColumns(array $definitions) + { + foreach ($definitions as $name => $options) { + $this->setColumn($name, $options['type'], $options['length'], $options); + } + } + + public function setColumn($name, $type, $length = null, $options = array(), $prepend = false) + { + if (is_string($options)) { + $options = explode('|', $options); + } + + foreach ($options as $k => $option) { + if (is_numeric($k)) { + if ( ! empty($option)) { + $options[$option] = true; + } + unset($options[$k]); + } + } + + // extract column name & field name + if (stripos($name, ' as ')) + { + if (strpos($name, ' as')) { + $parts = explode(' as ', $name); + } else { + $parts = explode(' AS ', $name); + } + + if (count($parts) > 1) { + $fieldName = $parts[1]; + } else { + $fieldName = $parts[0]; + } + + $name = strtolower($parts[0]); + } else { + $fieldName = $name; + $name = strtolower($name); + } + + $name = trim($name); + $fieldName = trim($fieldName); + + if ($prepend) { + $this->_columnNames = array_merge(array($fieldName => $name), $this->_columnNames); + $this->_fieldNames = array_merge(array($name => $fieldName), $this->_fieldNames); + } else { + $this->_columnNames[$fieldName] = $name; + $this->_fieldNames[$name] = $fieldName; + } + + if ($length == null) { + switch ($type) { + case 'string': + case 'clob': + case 'float': + case 'integer': + case 'array': + case 'object': + case 'blob': + case 'gzip': + // use php int max + $length = 2147483647; + break; + case 'boolean': + $length = 1; + case 'date': + // YYYY-MM-DD ISO 8601 + $length = 10; + case 'time': + // HH:NN:SS+00:00 ISO 8601 + $length = 14; + case 'timestamp': + // YYYY-MM-DDTHH:MM:SS+00:00 ISO 8601 + $length = 25; + break; + } + } + + $options['type'] = $type; + $options['length'] = $length; + + if ($prepend) { + $this->_columns = array_merge(array($name => $options), $this->_columns); + } else { + $this->_columns[$name] = $options; + } + + if (isset($options['primary']) && $options['primary']) { + if (isset($this->_identifier)) { + $this->_identifier = (array) $this->_identifier; + } + if ( ! in_array($fieldName, $this->_identifier)) { + $this->_identifier[] = $fieldName; + } + } + if (isset($options['default'])) { + $this->hasDefaultValues = true; + } + } + + public function hasDefaultValues() + { + return $this->hasDefaultValues; + } + + public function getDefaultValueOf($fieldName) + { + $columnName = $this->getColumnName($fieldName); + if ( ! isset($this->_columns[$columnName])) { + throw new IPF_ORM_Exception("Couldn't get default value. Column ".$columnName." doesn't exist."); + } + if (isset($this->_columns[$columnName]['default'])) { + return $this->_columns[$columnName]['default']; + } else { + return null; + } + } + + public function getIdentifier() + { + return $this->_identifier; + } + + public function getIdentifierType() + { + return $this->_identifierType; + } + + public function hasColumn($columnName) + { + return isset($this->_columns[strtolower($columnName)]); + } + + public function hasField($fieldName) + { + return isset($this->_columnNames[$fieldName]); + } + + public function setConnection(IPF_ORM_Connection $conn) + { + $this->_conn = $conn; + + $this->setParent($this->_conn); + + return $this; + } + + public function getConnection() + { + return $this->_conn; + } + + public function create(array $array = array()) + { + $record = new $this->_options['name']($this, true); + $record->fromArray($array); + + return $record; + } + + public function find($id, $hydrationMode = null) + { + if (is_null($id)) { + return false; + } + + $id = is_array($id) ? array_values($id) : array($id); + + $q = $this->createQuery(); + $q->where(implode(' = ? AND ', (array) $this->getIdentifier()) . ' = ?', $id) + ->limit(1); + $res = $q->fetchOne(array(), $hydrationMode); + $q->free(); + + return $res; + } + + public function findAll($hydrationMode = null) + { + return $this->createQuery()->execute(array(), $hydrationMode); + } + + public function findBySql($dql, $params = array(), $hydrationMode = null) + { + return $this->createQuery()->where($dql)->execute($params, $hydrationMode); + } + + public function findByDql($dql, $params = array(), $hydrationMode = null) + { + $parser = new IPF_ORM_Query($this->_conn); + $component = $this->getComponentName(); + $query = 'FROM ' . $component . ' WHERE ' . $dql; + + return $parser->query($query, $params, $hydrationMode); + } + + public function execute($queryKey, $params = array(), $hydrationMode = IPF_ORM::HYDRATE_RECORD) + { + return IPF_ORM_Manager::getInstance() + ->getQueryRegistry() + ->get($queryKey, $this->getComponentName()) + ->execute($params, $hydrationMode); + } + + public function executeOne($queryKey, $params = array(), $hydrationMode = IPF_ORM::HYDRATE_RECORD) + { + return IPF_ORM_Manager::getInstance() + ->getQueryRegistry() + ->get($queryKey, $this->getComponentName()) + ->fetchOne($params, $hydrationMode); + } + + public function clear() + { + $this->_identityMap = array(); + } + + public function addRecord(IPF_ORM_Record $record) + { + $id = implode(' ', $record->identifier()); + + if (isset($this->_identityMap[$id])) { + return false; + } + + $this->_identityMap[$id] = $record; + + return true; + } + + public function removeRecord(IPF_ORM_Record $record) + { + $id = implode(' ', $record->identifier()); + + if (isset($this->_identityMap[$id])) { + unset($this->_identityMap[$id]); + return true; + } + + return false; + } + + public function getRecord() + { + if ( ! empty($this->_data)) { + $identifierFieldNames = $this->getIdentifier(); + + if ( ! is_array($identifierFieldNames)) { + $identifierFieldNames = array($identifierFieldNames); + } + + $found = false; + foreach ($identifierFieldNames as $fieldName) { + if ( ! isset($this->_data[$fieldName])) { + // primary key column not found return new record + $found = true; + break; + } + $id[] = $this->_data[$fieldName]; + } + + if ($found) { + $recordName = $this->getComponentName(); + $record = new $recordName($this, true); + $this->_data = array(); + return $record; + } + + + $id = implode(' ', $id); + + if (isset($this->_identityMap[$id])) { + $record = $this->_identityMap[$id]; + $record->hydrate($this->_data); + } else { + $recordName = $this->getComponentName(); + $record = new $recordName($this); + $this->_identityMap[$id] = $record; + } + $this->_data = array(); + } else { + $recordName = $this->getComponentName(); + $record = new $recordName($this, true); + } + + return $record; + } + + public function getClassnameToReturn() + { + if ( ! isset($this->_options['subclasses'])) { + return $this->_options['name']; + } + foreach ($this->_options['subclasses'] as $subclass) { + $table = $this->_conn->getTable($subclass); + $inheritanceMap = $table->getOption('inheritanceMap'); + $nomatch = false; + foreach ($inheritanceMap as $key => $value) { + if ( ! isset($this->_data[$key]) || $this->_data[$key] != $value) { + $nomatch = true; + break; + } + } + if ( ! $nomatch) { + return $table->getComponentName(); + } + } + return $this->_options['name']; + } + + final public function getProxy($id = null) + { + if ($id !== null) { + $identifierColumnNames = $this->getIdentifierColumnNames(); + $query = 'SELECT ' . implode(', ', (array) $identifierColumnNames) + . ' FROM ' . $this->getTableName() + . ' WHERE ' . implode(' = ? && ', (array) $identifierColumnNames) . ' = ?'; + $query = $this->applyInheritance($query); + + $params = array_merge(array($id), array_values($this->_options['inheritanceMap'])); + + $this->_data = $this->_conn->execute($query, $params)->fetch(PDO::FETCH_ASSOC); + + if ($this->_data === false) + return false; + } + return $this->getRecord(); + } + + final public function applyInheritance($where) + { + if ( ! empty($this->_options['inheritanceMap'])) { + $a = array(); + foreach ($this->_options['inheritanceMap'] as $field => $value) { + $a[] = $this->getColumnName($field) . ' = ?'; + } + $i = implode(' AND ', $a); + $where .= ' AND ' . $i; + } + return $where; + } + + public function count() + { + $a = $this->_conn->execute('SELECT COUNT(1) FROM ' . $this->_options['tableName'])->fetch(IPF_ORM::FETCH_NUM); + return current($a); + } + + public function getQueryObject() + { + $graph = new IPF_ORM_Query($this->getConnection()); + $graph->load($this->getComponentName()); + return $graph; + } + + public function getEnumValues($fieldName) + { + $columnName = $this->getColumnName($fieldName); + if (isset($this->_columns[$columnName]['values'])) { + return $this->_columns[$columnName]['values']; + } else { + return array(); + } + } + + public function enumValue($fieldName, $index) + { + if ($index instanceof IPF_ORM_Null) { + return $index; + } + + $columnName = $this->getColumnName($fieldName); + if ( ! $this->_conn->getAttribute(IPF_ORM::ATTR_USE_NATIVE_ENUM) + && isset($this->_columns[$columnName]['values'][$index]) + ) { + return $this->_columns[$columnName]['values'][$index]; + } + + return $index; + } + + public function enumIndex($fieldName, $value) + { + $values = $this->getEnumValues($fieldName); + + $index = array_search($value, $values); + if ($index === false || !$this->_conn->getAttribute(IPF_ORM::ATTR_USE_NATIVE_ENUM)) { + return $index; + } + return $value; + } + + public function validateField($fieldName, $value, IPF_ORM_Record $record = null) + { + if ($record instanceof IPF_ORM_Record) { + $errorStack = $record->getErrorStack(); + } else { + $record = $this->create(); + $errorStack = new IPF_ORM_Validator_ErrorStack($this->getOption('name')); + } + + if ($value === self::$_null) { + $value = null; + } else if ($value instanceof IPF_ORM_Record) { + $value = $value->getIncremented(); + } + + $dataType = $this->getTypeOf($fieldName); + + // Validate field type, if type validation is enabled + if ($this->getAttribute(IPF_ORM::ATTR_VALIDATE) & IPF_ORM::VALIDATE_TYPES) { + if ( ! IPF_ORM_Validator::isValidType($value, $dataType)) { + $errorStack->add($fieldName, 'type'); + } + if ($dataType == 'enum') { + $enumIndex = $this->enumIndex($fieldName, $value); + if ($enumIndex === false) { + $errorStack->add($fieldName, 'enum'); + } + } + } + + // Validate field length, if length validation is enabled + if ($this->getAttribute(IPF_ORM::ATTR_VALIDATE) & IPF_ORM::VALIDATE_LENGTHS) { + if ( ! IPF_ORM_Validator::validateLength($value, $dataType, $this->getFieldLength($fieldName))) { + $errorStack->add($fieldName, 'length'); + } + } + + // Run all custom validators + foreach ($this->getFieldValidators($fieldName) as $validatorName => $args) { + if ( ! is_string($validatorName)) { + $validatorName = $args; + $args = array(); + } + + $validator = IPF_ORM_Validator::getValidator($validatorName); + $validator->invoker = $record; + $validator->field = $fieldName; + $validator->args = $args; + if ( ! $validator->validate($value)) { + $errorStack->add($fieldName, $validator); + } + } + + return $errorStack; + } + + public function getColumnCount() + { + return $this->columnCount; + } + + public function getColumns() + { + return $this->_columns; + } + + public function removeColumn($fieldName) + { + if ( ! $this->hasField($fieldName)) { + return false; + } + + $columnName = $this->getColumnName($fieldName); + unset($this->_columnNames[$fieldName], $this->_fieldNames[$columnName], $this->_columns[$columnName]); + $this->columnCount = count($this->_columns); + return true; + } + + public function getColumnNames(array $fieldNames = null) + { + if ($fieldNames === null) { + return array_keys($this->_columns); + } else { + $columnNames = array(); + foreach ($fieldNames as $fieldName) { + $columnNames[] = $this->getColumnName($fieldName); + } + return $columnNames; + } + } + + public function getIdentifierColumnNames() + { + return $this->getColumnNames((array) $this->getIdentifier()); + } + + public function getFieldNames() + { + return array_values($this->_fieldNames); + } + + public function getDefinitionOf($fieldName) + { + $columnName = $this->getColumnName($fieldName); + return $this->getColumnDefinition($columnName); + } + + public function getTypeOf($fieldName) + { + return $this->getTypeOfColumn($this->getColumnName($fieldName)); + } + + public function getTypeOfColumn($columnName) + { + return isset($this->_columns[$columnName]) ? $this->_columns[$columnName]['type'] : false; + } + + public function setData(array $data) + { + $this->_data = $data; + } + + public function getData() + { + return $this->_data; + } + + public function prepareValue($fieldName, $value, $typeHint = null) + { + if ($value === self::$_null) { + return self::$_null; + } else if ($value === null) { + return null; + } else { + $type = is_null($typeHint) ? $this->getTypeOf($fieldName) : $typeHint; + + switch ($type) { + case 'integer': + case 'string'; + // don't do any casting here PHP INT_MAX is smaller than what the databases support + break; + case 'enum': + return $this->enumValue($fieldName, $value); + break; + case 'boolean': + return (boolean) $value; + break; + case 'array': + case 'object': + if (is_string($value)) { + $value = empty($value) ? null:unserialize($value); + + if ($value === false) { + throw new IPF_ORM_Exception('Unserialization of ' . $fieldName . ' failed.'); + } + return $value; + } + break; + case 'gzip': + $value = gzuncompress($value); + + if ($value === false) { + throw new IPF_ORM_Exception('Uncompressing of ' . $fieldName . ' failed.'); + } + return $value; + break; + } + } + return $value; + } + + public function getTree() + { + if (isset($this->_options['treeImpl'])) { + if ( ! $this->_tree) { + $options = isset($this->_options['treeOptions']) ? $this->_options['treeOptions'] : array(); + $this->_tree = IPF_ORM_Tree::factory($this, + $this->_options['treeImpl'], + $options + ); + } + return $this->_tree; + } + return false; + } + + public function getComponentName() + { + return $this->_options['name']; + } + + public function getTableName() + { + return $this->_options['tableName']; + } + + public function setTableName($tableName) + { + $this->setOption('tableName', $this->_conn->formatter->getTableName($tableName)); + } + + public function isTree() + { + return ( ! is_null($this->_options['treeImpl'])) ? true : false; + } + + public function getTemplate($template) + { + if ( ! isset($this->_templates[$template])) { + throw new IPF_ORM_Exception('Template ' . $template . ' not loaded'); + } + + return $this->_templates[$template]; + } + + public function hasTemplate($template) + { + return isset($this->_templates[$template]); + } + + public function addTemplate($template, IPF_ORM_Template $impl) + { + $this->_templates[$template] = $impl; + + return $this; + } + + public function getGenerators() + { + return $this->_generators; + } + + public function getGenerator($generator) + { + if ( ! isset($this->_generators[$generator])) { + throw new IPF_ORM_Exception('Generator ' . $generator . ' not loaded'); + } + + return $this->_generators[$generator]; + } + + public function hasGenerator($generator) + { + return isset($this->_generators[$generator]); + } + + public function addGenerator(IPF_ORM_Record_Generator $generator, $name = null) + { + if ($name === null) { + $this->_generators[] = $generator; + } else { + $this->_generators[$name] = $generator; + } + return $this; + } + + public function bindQueryParts(array $queryParts) + { + $this->_options['queryParts'] = $queryParts; + + return $this; + } + + public function bindQueryPart($queryPart, $value) + { + $this->_options['queryParts'][$queryPart] = $value; + + return $this; + } + + public function getFieldValidators($fieldName) + { + $validators = array(); + $columnName = $this->getColumnName($fieldName); + // this loop is a dirty workaround to get the validators filtered out of + // the options, since everything is squeezed together currently + foreach ($this->_columns[$columnName] as $name => $args) { + if (empty($name) + || $name == 'primary' + || $name == 'protected' + || $name == 'autoincrement' + || $name == 'default' + || $name == 'values' + || $name == 'sequence' + || $name == 'zerofill' + || $name == 'owner' + || $name == 'scale' + || $name == 'type' + || $name == 'length' + || $name == 'fixed') { + continue; + } + if ($name == 'notnull' && isset($this->_columns[$columnName]['autoincrement'])) { + continue; + } + // skip it if it's explicitly set to FALSE (i.e. notnull => false) + if ($args === false) { + continue; + } + $validators[$name] = $args; + } + + return $validators; + } + + public function getFieldLength($fieldName) + { + return $this->_columns[$this->getColumnName($fieldName)]['length']; + } + + public function getBoundQueryPart($queryPart) + { + if ( ! isset($this->_options['queryParts'][$queryPart])) { + return null; + } + + return $this->_options['queryParts'][$queryPart]; + } + + public function unshiftFilter(IPF_ORM_Record_Filter $filter) + { + $filter->setTable($this); + + $filter->init(); + + array_unshift($this->_filters, $filter); + + return $this; + } + + public function getFilters() + { + return $this->_filters; + } + + public function __toString() + { + return IPF_ORM_Utils::getTableAsString($this); + } + + protected function findBy($fieldName, $value, $hydrationMode = null) + { + return $this->createQuery()->where($fieldName . ' = ?', array($value))->execute(array(), $hydrationMode); + } + + protected function findOneBy($fieldName, $value, $hydrationMode = null) + { + $results = $this->createQuery() + ->where($fieldName . ' = ?',array($value)) + ->limit(1) + ->execute(array(), $hydrationMode); + + if (is_array($results) && isset($results[0])) { + return $results[0]; + } else if ($results instanceof IPF_ORM_Collection && $results->count() > 0) { + return $results->getFirst(); + } else { + return false; + } + } + + protected function _resolveFindByFieldName($name) + { + $fieldName = IPF_ORM_Inflector::tableize($name); + if ($this->hasColumn($name) || $this->hasField($name)) { + return $this->getFieldName($this->getColumnName($name)); + } else if ($this->hasColumn($fieldName) || $this->hasField($fieldName)) { + return $this->getFieldName($this->getColumnName($fieldName)); + } else { + return false; + } + } + + public function __call($method, $arguments) + { + if (substr($method, 0, 6) == 'findBy') { + $by = substr($method, 6, strlen($method)); + $method = 'findBy'; + } else if (substr($method, 0, 9) == 'findOneBy') { + $by = substr($method, 9, strlen($method)); + $method = 'findOneBy'; + } + + if (isset($by)) { + if ( ! isset($arguments[0])) { + throw new IPF_ORM_Exception('You must specify the value to findBy'); + } + + $fieldName = $this->_resolveFindByFieldName($by); + $hydrationMode = isset($arguments[1]) ? $arguments[1]:null; + if ($this->hasField($fieldName)) { + return $this->$method($fieldName, $arguments[0], $hydrationMode); + } else if ($this->hasRelation($by)) { + $relation = $this->getRelation($by); + + if ($relation['type'] === IPF_ORM_Relation::MANY) { + throw new IPF_ORM_Exception('Cannot findBy many relationship.'); + } + + return $this->$method($relation['local'], $arguments[0], $hydrationMode); + } else { + throw new IPF_ORM_Exception('Cannot find by: ' . $by . '. Invalid column or relationship alias.'); + } + } + + throw new IPF_ORM_Exception(sprintf('Unknown method %s::%s', get_class($this), $method)); + } +} diff --git a/ipf/orm/table/repository.php b/ipf/orm/table/repository.php new file mode 100644 index 0000000..1fd1557 --- /dev/null +++ b/ipf/orm/table/repository.php @@ -0,0 +1,77 @@ +table = $table; + } + + public function getTable() + { + return $this->table; + } + + public function add(IPF_ORM_Record $record) + { + $oid = $record->getOID(); + + if (isset($this->registry[$oid])) { + return false; + } + $this->registry[$oid] = $record; + + return true; + } + + public function get($oid) + { + if ( ! isset($this->registry[$oid])) { + throw new IPF_ORM_Exception("Unknown object identifier"); + } + return $this->registry[$oid]; + } + + public function count() + { + return count($this->registry); + } + + public function evict($oid) + { + if ( ! isset($this->registry[$oid])) { + return false; + } + unset($this->registry[$oid]); + return true; + } + + public function evictAll() + { + $evicted = 0; + foreach ($this->registry as $oid=>$record) { + if ($this->evict($oid)) { + $evicted++; + } + } + return $evicted; + } + + public function getIterator() + { + return new ArrayIterator($this->registry); + } + + public function contains($oid) + { + return isset($this->registry[$oid]); + } + + public function loadAll() + { + $this->table->findAll(); + } +} \ No newline at end of file diff --git a/ipf/orm/template.php b/ipf/orm/template.php new file mode 100644 index 0000000..3de6e4d --- /dev/null +++ b/ipf/orm/template.php @@ -0,0 +1,58 @@ +_table = $table; + } + + public function getTable() + { + return $this->_table; + } + + public function setInvoker(IPF_ORM_Record $invoker) + { + $this->_invoker = $invoker; + } + + public function getInvoker() + { + return $this->_invoker; + } + + public function addChild(IPF_ORM_Template $template) + { + $this->_plugin->addChild($template); + + return $this; + } + + public function getPlugin() + { + return $this->_plugin; + } + + public function get($name) + { + throw new IPF_ORM_Exception("Templates doesn't support accessors."); + } + + public function set($name, $value) + { + throw new IPF_ORM_Exception("Templates doesn't support accessors."); + } + + public function setUp() + { + + } + + public function setTableDefinition() + { + + } +} \ No newline at end of file diff --git a/ipf/orm/template/listener/timestampable.php b/ipf/orm/template/listener/timestampable.php new file mode 100644 index 0000000..6905d7b --- /dev/null +++ b/ipf/orm/template/listener/timestampable.php @@ -0,0 +1,48 @@ +_options = $options; + } + + public function preInsert(IPF_ORM_Event $event) + { + if( ! $this->_options['created']['disabled']) { + $createdName = $this->_options['created']['name']; + $event->getInvoker()->$createdName = $this->getTimestamp('created'); + } + + if( ! $this->_options['updated']['disabled'] && $this->_options['updated']['onInsert']) { + $updatedName = $this->_options['updated']['name']; + $event->getInvoker()->$updatedName = $this->getTimestamp('updated'); + } + } + + public function preUpdate(IPF_ORM_Event $event) + { + if( ! $this->_options['updated']['disabled']) { + $updatedName = $this->_options['updated']['name']; + $event->getInvoker()->$updatedName = $this->getTimestamp('updated'); + } + } + + public function getTimestamp($type) + { + $options = $this->_options[$type]; + + if ($options['expression'] !== false && is_string($options['expression'])) { + return new IPF_ORM_Expression($options['expression']); + } else { + if ($options['type'] == 'date') { + return date($options['format'], time()); + } else if ($options['type'] == 'timestamp') { + return date($options['format'], time()); + } else { + return time(); + } + } + } +} \ No newline at end of file diff --git a/ipf/orm/template/timestampable.php b/ipf/orm/template/timestampable.php new file mode 100644 index 0000000..f39a19b --- /dev/null +++ b/ipf/orm/template/timestampable.php @@ -0,0 +1,34 @@ + array('name' => 'created_at', + 'type' => 'timestamp', + 'format' => 'Y-m-d H:i:s', + 'disabled' => false, + 'expression' => false, + 'options' => array()), + 'updated' => array('name' => 'updated_at', + 'type' => 'timestamp', + 'format' => 'Y-m-d H:i:s', + 'disabled' => false, + 'expression' => false, + 'onInsert' => true, + 'options' => array())); + + public function __construct(array $options = array()) + { + $this->_options = IPF_ORM_Utils::arrayDeepMerge($this->_options, $options); + } + + public function setTableDefinition() + { + if( ! $this->_options['created']['disabled']) { + $this->hasColumn($this->_options['created']['name'], $this->_options['created']['type'], null, $this->_options['created']['options']); + } + if( ! $this->_options['updated']['disabled']) { + $this->hasColumn($this->_options['updated']['name'], $this->_options['updated']['type'], null, $this->_options['updated']['options']); + } + $this->addListener(new IPF_ORM_Template_Listener_Timestampable($this->_options)); + } +} \ No newline at end of file diff --git a/ipf/orm/transaction.php b/ipf/orm/transaction.php new file mode 100644 index 0000000..b4d08d6 --- /dev/null +++ b/ipf/orm/transaction.php @@ -0,0 +1,279 @@ +_collections[] = $coll; + + return $this; + } + + public function getState() + { + switch ($this->_nestingLevel) { + case 0: + return IPF_ORM_Transaction::STATE_SLEEP; + break; + case 1: + return IPF_ORM_Transaction::STATE_ACTIVE; + break; + default: + return IPF_ORM_Transaction::STATE_BUSY; + } + } + + public function addInvalid(IPF_ORM_Record $record) + { + if (in_array($record, $this->invalid, true)) { + return false; + } + $this->invalid[] = $record; + return true; + } + + public function getInvalid() + { + return $this->invalid; + } + + public function getTransactionLevel() + { + return $this->_nestingLevel; + } + + public function getInternalTransactionLevel() + { + return $this->_internalNestingLevel; + } + + public function beginTransaction($savepoint = null) + { + $this->conn->connect(); + $listener = $this->conn->getAttribute(IPF_ORM::ATTR_LISTENER); + + + if ( ! is_null($savepoint)) { + $this->savePoints[] = $savepoint; + + $event = new IPF_ORM_Event($this, IPF_ORM_Event::SAVEPOINT_CREATE); + + $listener->preSavepointCreate($event); + + if ( ! $event->skipOperation) { + $this->createSavePoint($savepoint); + } + + $listener->postSavepointCreate($event); + } else { + if ($this->_nestingLevel == 0) { + $event = new IPF_ORM_Event($this, IPF_ORM_Event::TX_BEGIN); + + $listener->preTransactionBegin($event); + + if ( ! $event->skipOperation) { + try { + $this->_doBeginTransaction(); + } catch (Exception $e) { + throw new IPF_ORM_Exception($e->getMessage()); + } + } + $listener->postTransactionBegin($event); + } + } + + $level = ++$this->_nestingLevel; + + return $level; + } + + public function commit($savepoint = null) + { + if ($this->_nestingLevel == 0) { + throw new IPF_ORM_Exception("Commit failed. There is no active transaction."); + } + + $this->conn->connect(); + + $listener = $this->conn->getAttribute(IPF_ORM::ATTR_LISTENER); + + if ( ! is_null($savepoint)) { + $this->_nestingLevel -= $this->removeSavePoints($savepoint); + + $event = new IPF_ORM_Event($this, IPF_ORM_Event::SAVEPOINT_COMMIT); + + $listener->preSavepointCommit($event); + + if ( ! $event->skipOperation) { + $this->releaseSavePoint($savepoint); + } + + $listener->postSavepointCommit($event); + } else { + + if ($this->_nestingLevel == 1 || $this->_internalNestingLevel == 1) { + if ( ! empty($this->invalid)) { + if ($this->_internalNestingLevel == 1) { + // transaction was started by IPF_ORM, so we are responsible + // for a rollback + $this->rollback(); + $tmp = $this->invalid; + $this->invalid = array(); + throw new IPF_ORM_Exception_Validator($tmp); + } + } + if ($this->_nestingLevel == 1) { + // take snapshots of all collections used within this transaction + foreach ($this->_collections as $coll) { + $coll->takeSnapshot(); + } + $this->_collections = array(); + + $event = new IPF_ORM_Event($this, IPF_ORM_Event::TX_COMMIT); + + $listener->preTransactionCommit($event); + if ( ! $event->skipOperation) { + $this->_doCommit(); + } + $listener->postTransactionCommit($event); + } + } + + if ($this->_nestingLevel > 0) { + $this->_nestingLevel--; + } + if ($this->_internalNestingLevel > 0) { + $this->_internalNestingLevel--; + } + } + + return true; + } + + public function rollback($savepoint = null) + { + if ($this->_nestingLevel == 0) { + throw new IPF_ORM_Exception("Rollback failed. There is no active transaction."); + } + + $this->conn->connect(); + + if ($this->_internalNestingLevel > 1 || $this->_nestingLevel > 1) { + $this->_internalNestingLevel--; + $this->_nestingLevel--; + return false; + } + + $listener = $this->conn->getAttribute(IPF_ORM::ATTR_LISTENER); + + if ( ! is_null($savepoint)) { + $this->_nestingLevel -= $this->removeSavePoints($savepoint); + + $event = new IPF_ORM_Event($this, IPF_ORM_Event::SAVEPOINT_ROLLBACK); + + $listener->preSavepointRollback($event); + + if ( ! $event->skipOperation) { + $this->rollbackSavePoint($savepoint); + } + + $listener->postSavepointRollback($event); + } else { + $event = new IPF_ORM_Event($this, IPF_ORM_Event::TX_ROLLBACK); + + $listener->preTransactionRollback($event); + + if ( ! $event->skipOperation) { + $this->_nestingLevel = 0; + $this->_internalNestingLevel = 0; + try { + $this->_doRollback(); + } catch (Exception $e) { + throw new IPF_ORM_Exception($e->getMessage()); + } + } + + $listener->postTransactionRollback($event); + } + + return true; + } + + protected function createSavePoint($savepoint) + { + throw new IPF_ORM_Exception('Savepoints not supported by this driver.'); + } + + protected function releaseSavePoint($savepoint) + { + throw new IPF_ORM_Exception('Savepoints not supported by this driver.'); + } + + protected function rollbackSavePoint($savepoint) + { + throw new IPF_ORM_Exception('Savepoints not supported by this driver.'); + } + + protected function _doRollback() + { + $this->conn->getDbh()->rollback(); + } + + protected function _doCommit() + { + $this->conn->getDbh()->commit(); + } + + protected function _doBeginTransaction() + { + $this->conn->getDbh()->beginTransaction(); + } + + private function removeSavePoints($savepoint) + { + $this->savePoints = array_values($this->savePoints); + + $found = false; + $i = 0; + + foreach ($this->savePoints as $key => $sp) { + if ( ! $found) { + if ($sp === $savepoint) { + $found = true; + } + } + if ($found) { + $i++; + unset($this->savePoints[$key]); + } + } + + return $i; + } + + public function setIsolation($isolation) + { + throw new IPF_ORM_Exception('Transaction isolation levels not supported by this driver.'); + } + + public function getIsolation() + { + throw new IPF_ORM_Exception('Fetching transaction isolation level not supported by this driver.'); + } + + public function beginInternalTransaction($savepoint = null) + { + $this->_internalNestingLevel++; + return $this->beginTransaction($savepoint); + } +} diff --git a/ipf/orm/transaction/mysql.php b/ipf/orm/transaction/mysql.php new file mode 100644 index 0000000..20597ff --- /dev/null +++ b/ipf/orm/transaction/mysql.php @@ -0,0 +1,47 @@ +conn->execute($query); + } + + protected function releaseSavePoint($savepoint) + { + $query = 'RELEASE SAVEPOINT ' . $savepoint; + + return $this->conn->execute($query); + } + + protected function rollbackSavePoint($savepoint) + { + $query = 'ROLLBACK TO SAVEPOINT ' . $savepoint; + + return $this->conn->execute($query); + } + + public function setIsolation($isolation) + { + switch ($isolation) { + case 'READ UNCOMMITTED': + case 'READ COMMITTED': + case 'REPEATABLE READ': + case 'SERIALIZABLE': + break; + default: + throw new IPF_ORM_Exception('Isolation level ' . $isolation . ' is not supported.'); + } + + $query = 'SET SESSION TRANSACTION ISOLATION LEVEL ' . $isolation; + + return $this->conn->execute($query); + } + + public function getIsolation() + { + return $this->conn->fetchOne('SELECT @@tx_isolation'); + } +} \ No newline at end of file diff --git a/ipf/orm/utils.php b/ipf/orm/utils.php new file mode 100644 index 0000000..5afd7da --- /dev/null +++ b/ipf/orm/utils.php @@ -0,0 +1,195 @@ +'; + $r[] = 'IPF_ORM_Connection object'; + $r[] = 'State : ' . IPF_ORM_Utils::getConnectionStateAsString($connection->transaction->getState()); + $r[] = 'Open Transactions : ' . $connection->transaction->getTransactionLevel(); + $r[] = 'Table in memory : ' . $connection->count(); + $r[] = 'Driver name : ' . $connection->getAttribute(IPF_ORM::ATTR_DRIVER_NAME); + $r[] = ""; + return implode("\n",$r)."
    "; + } + + public static function getValidators() + { + return array( + 'country', + 'creditcard', + 'date', + 'driver', + 'email', + 'exception', + 'future', + 'htmlcolor', + 'ip', + 'minlength', + 'nospace', + 'notblank', + 'notnull', + 'past', + 'range', + 'readonly', + 'regexp', + 'time', + 'timestamp', + 'unique', + 'unsigned', + 'usstate', + ); + } + + + public static function arrayDeepMerge() + { + switch (func_num_args()) { + case 0: + return false; + case 1: + return func_get_arg(0); + case 2: + $args = func_get_args(); + $args[2] = array(); + + if (is_array($args[0]) && is_array($args[1])) + { + foreach (array_unique(array_merge(array_keys($args[0]),array_keys($args[1]))) as $key) + { + $isKey0 = array_key_exists($key, $args[0]); + $isKey1 = array_key_exists($key, $args[1]); + + if ($isKey0 && $isKey1 && is_array($args[0][$key]) && is_array($args[1][$key])) + { + $args[2][$key] = self::arrayDeepMerge($args[0][$key], $args[1][$key]); + } else if ($isKey0 && $isKey1) { + $args[2][$key] = $args[1][$key]; + } else if ( ! $isKey1) { + $args[2][$key] = $args[0][$key]; + } else if ( ! $isKey0) { + $args[2][$key] = $args[1][$key]; + } + } + + return $args[2]; + } else { + return $args[1]; + } + default: + $args = func_get_args(); + $args[1] = self::arrayDeepMerge($args[0], $args[1]); + array_shift($args); + return call_user_func_array(array('IPF', 'arrayDeepMerge'), $args); + break; + } + } + + public static function makeDirectories($path, $mode = 0777) + { + if ( ! $path) { + return false; + } + + if (is_dir($path) || is_file($path)) { + return true; + } + + return mkdir(trim($path), $mode, true); + } + + public static function removeDirectories($folderPath) + { + if (is_dir($folderPath)) + { + foreach (scandir($folderPath) as $value) + { + if ($value != '.' && $value != '..') + { + $value = $folderPath . "/" . $value; + + if (is_dir($value)) { + self::removeDirectories($value); + } else if (is_file($value)) { + unlink($value); + } + } + } + + return rmdir($folderPath); + } else { + return false; + } + } + + public static function copyDirectory($source, $dest) + { + // Simple copy for a file + if (is_file($source)) { + return copy($source, $dest); + } + + // Make destination directory + if ( ! is_dir($dest)) { + mkdir($dest); + } + + // Loop through the folder + $dir = dir($source); + while (false !== $entry = $dir->read()) { + // Skip pointers + if ($entry == '.' || $entry == '..') { + continue; + } + + // Deep copy directories + if ($dest !== "$source/$entry") { + self::copyDirectory("$source/$entry", "$dest/$entry"); + } + } + + // Clean up + $dir->close(); + + return true; + } + + public static function getTableAsString(IPF_ORM_Table $table) + { + $r[] = "
    ";
    +        $r[] = "Component   : ".$table->getComponentName();
    +        $r[] = "Table       : ".$table->getTableName();
    +        $r[] = "
    "; + + return implode("\n",$r)."
    "; + } + + public static function getCollectionAsString(IPF_ORM_Collection $collection) + { + $r[] = "
    ";
    +        $r[] = get_class($collection);
    +        $r[] = 'data : ' . IPF_ORM::dump($collection->getData(), false);
    +        //$r[] = 'snapshot : ' . IPF_ORM::dump($collection->getSnapshot());
    +        $r[] = "
    "; + return implode("\n",$r); + } + + +} + diff --git a/ipf/orm/validator.php b/ipf/orm/validator.php new file mode 100644 index 0000000..0fd7340 --- /dev/null +++ b/ipf/orm/validator.php @@ -0,0 +1,98 @@ +getTable(); + + // if record is transient all fields will be validated + // if record is persistent only the modified fields will be validated + $fields = $record->exists() ? $record->getModified():$record->getData(); + foreach ($fields as $fieldName => $value) { + $table->validateField($fieldName, $value, $record); + } + } + + public static function validateLength($value, $type, $maximumLength) + { + if ($type == 'timestamp' || $type == 'integer' || $type == 'enum') { + return true; + } else if ($type == 'array' || $type == 'object') { + $length = strlen(serialize($value)); + } else { + $length = strlen($value); + } + if ($length > $maximumLength) { + return false; + } + return true; + } + + public function hasErrors() + { + return (count($this->stack) > 0); + } + + public static function isValidType($var, $type) + { + if ($var instanceof IPF_ORM_Expression) { + return true; + } else if ($var === null) { + return true; + } else if (is_object($var)) { + return $type == 'object'; + } + + switch ($type) { + case 'float': + case 'double': + case 'decimal': + return (string)$var == strval(floatval($var)); + case 'integer': + return (string)$var == strval(intval($var)); + case 'string': + return is_string($var) || is_numeric($var); + case 'blob': + case 'clob': + case 'gzip': + return is_string($var); + case 'array': + return is_array($var); + case 'object': + return is_object($var); + case 'boolean': + return is_bool($var) || (is_numeric($var) && ($var == 0 || $var == 1)); + case 'timestamp': + $validator = self::getValidator('timestamp'); + return $validator->validate($var); + case 'time': + $validator = self::getValidator('time'); + return $validator->validate($var); + case 'date': + $validator = self::getValidator('date'); + return $validator->validate($var); + case 'enum': + return is_string($var) || is_int($var); + default: + return false; + } + } +} \ No newline at end of file diff --git a/ipf/orm/validator/country.php b/ipf/orm/validator/country.php new file mode 100644 index 0000000..2536f15 --- /dev/null +++ b/ipf/orm/validator/country.php @@ -0,0 +1,260 @@ + 'Andorra', + 'ae' => 'United Arab Emirates', + 'af' => 'Afghanistan', + 'ag' => 'Antigua and Barbuda', + 'ai' => 'Anguilla', + 'al' => 'Albania', + 'am' => 'Armenia', + 'an' => 'Netherlands Antilles', + 'ao' => 'Angola', + 'aq' => 'Antarctica', + 'ar' => 'Argentina', + 'as' => 'American Samoa', + 'at' => 'Austria', + 'au' => 'Australia', + 'aw' => 'Aruba', + 'az' => 'Azerbaijan', + 'ba' => 'Bosnia Hercegovina', + 'bb' => 'Barbados', + 'bd' => 'Bangladesh', + 'be' => 'Belgium', + 'bf' => 'Burkina Faso', + 'bg' => 'Bulgaria', + 'bh' => 'Bahrain', + 'bi' => 'Burundi', + 'bj' => 'Benin', + 'bm' => 'Bermuda', + 'bn' => 'Brunei Darussalam', + 'bo' => 'Bolivia', + 'br' => 'Brazil', + 'bs' => 'Bahamas', + 'bt' => 'Bhutan', + 'bv' => 'Bouvet Island', + 'bw' => 'Botswana', + 'by' => 'Belarus (Byelorussia)', + 'bz' => 'Belize', + 'ca' => 'Canada', + 'cc' => 'Cocos Islands', + 'cd' => 'Congo, The Democratic Republic of the', + 'cf' => 'Central African Republic', + 'cg' => 'Congo', + 'ch' => 'Switzerland', + 'ci' => 'Ivory Coast', + 'ck' => 'Cook Islands', + 'cl' => 'Chile', + 'cm' => 'Cameroon', + 'cn' => 'China', + 'co' => 'Colombia', + 'cr' => 'Costa Rica', + 'cs' => 'Czechoslovakia', + 'cu' => 'Cuba', + 'cv' => 'Cape Verde', + 'cx' => 'Christmas Island', + 'cy' => 'Cyprus', + 'cz' => 'Czech Republic', + 'de' => 'Germany', + 'dj' => 'Djibouti', + 'dk' => 'Denmark', + 'dm' => 'Dominica', + 'do' => 'Dominican Republic', + 'dz' => 'Algeria', + 'ec' => 'Ecuador', + 'ee' => 'Estonia', + 'eg' => 'Egypt', + 'eh' => 'Western Sahara', + 'er' => 'Eritrea', + 'es' => 'Spain', + 'et' => 'Ethiopia', + 'fi' => 'Finland', + 'fj' => 'Fiji', + 'fk' => 'Falkland Islands', + 'fm' => 'Micronesia', + 'fo' => 'Faroe Islands', + 'fr' => 'France', + 'fx' => 'France, Metropolitan FX', + 'ga' => 'Gabon', + 'gb' => 'United Kingdom (Great Britain)', + 'gd' => 'Grenada', + 'ge' => 'Georgia', + 'gf' => 'French Guiana', + 'gh' => 'Ghana', + 'gi' => 'Gibraltar', + 'gl' => 'Greenland', + 'gm' => 'Gambia', + 'gn' => 'Guinea', + 'gp' => 'Guadeloupe', + 'gq' => 'Equatorial Guinea', + 'gr' => 'Greece', + 'gs' => 'South Georgia and the South Sandwich Islands', + 'gt' => 'Guatemala', + 'gu' => 'Guam', + 'gw' => 'Guinea-bissau', + 'gy' => 'Guyana', + 'hk' => 'Hong Kong', + 'hm' => 'Heard and McDonald Islands', + 'hn' => 'Honduras', + 'hr' => 'Croatia', + 'ht' => 'Haiti', + 'hu' => 'Hungary', + 'id' => 'Indonesia', + 'ie' => 'Ireland', + 'il' => 'Israel', + 'in' => 'India', + 'io' => 'British Indian Ocean Territory', + 'iq' => 'Iraq', + 'ir' => 'Iran', + 'is' => 'Iceland', + 'it' => 'Italy', + 'jm' => 'Jamaica', + 'jo' => 'Jordan', + 'jp' => 'Japan', + 'ke' => 'Kenya', + 'kg' => 'Kyrgyzstan', + 'kh' => 'Cambodia', + 'ki' => 'Kiribati', + 'km' => 'Comoros', + 'kn' => 'Saint Kitts and Nevis', + 'kp' => 'North Korea', + 'kr' => 'South Korea', + 'kw' => 'Kuwait', + 'ky' => 'Cayman Islands', + 'kz' => 'Kazakhstan', + 'la' => 'Laos', + 'lb' => 'Lebanon', + 'lc' => 'Saint Lucia', + 'li' => 'Lichtenstein', + 'lk' => 'Sri Lanka', + 'lr' => 'Liberia', + 'ls' => 'Lesotho', + 'lt' => 'Lithuania', + 'lu' => 'Luxembourg', + 'lv' => 'Latvia', + 'ly' => 'Libya', + 'ma' => 'Morocco', + 'mc' => 'Monaco', + 'md' => 'Moldova Republic', + 'mg' => 'Madagascar', + 'mh' => 'Marshall Islands', + 'mk' => 'Macedonia, The Former Yugoslav Republic of', + 'ml' => 'Mali', + 'mm' => 'Myanmar', + 'mn' => 'Mongolia', + 'mo' => 'Macau', + 'mp' => 'Northern Mariana Islands', + 'mq' => 'Martinique', + 'mr' => 'Mauritania', + 'ms' => 'Montserrat', + 'mt' => 'Malta', + 'mu' => 'Mauritius', + 'mv' => 'Maldives', + 'mw' => 'Malawi', + 'mx' => 'Mexico', + 'my' => 'Malaysia', + 'mz' => 'Mozambique', + 'na' => 'Namibia', + 'nc' => 'New Caledonia', + 'ne' => 'Niger', + 'nf' => 'Norfolk Island', + 'ng' => 'Nigeria', + 'ni' => 'Nicaragua', + 'nl' => 'Netherlands', + 'no' => 'Norway', + 'np' => 'Nepal', + 'nr' => 'Nauru', + 'nt' => 'Neutral Zone', + 'nu' => 'Niue', + 'nz' => 'New Zealand', + 'om' => 'Oman', + 'pa' => 'Panama', + 'pe' => 'Peru', + 'pf' => 'French Polynesia', + 'pg' => 'Papua New Guinea', + 'ph' => 'Philippines', + 'pk' => 'Pakistan', + 'pl' => 'Poland', + 'pm' => 'St. Pierre and Miquelon', + 'pn' => 'Pitcairn', + 'pr' => 'Puerto Rico', + 'pt' => 'Portugal', + 'pw' => 'Palau', + 'py' => 'Paraguay', + 'qa' => 'Qatar', + 're' => 'Reunion', + 'ro' => 'Romania', + 'ru' => 'Russia', + 'rw' => 'Rwanda', + 'sa' => 'Saudi Arabia', + 'sb' => 'Solomon Islands', + 'sc' => 'Seychelles', + 'sd' => 'Sudan', + 'se' => 'Sweden', + 'sg' => 'Singapore', + 'sh' => 'St. Helena', + 'si' => 'Slovenia', + 'sj' => 'Svalbard and Jan Mayen Islands', + 'sk' => 'Slovakia (Slovak Republic)', + 'sl' => 'Sierra Leone', + 'sm' => 'San Marino', + 'sn' => 'Senegal', + 'so' => 'Somalia', + 'sr' => 'Suriname', + 'st' => 'Sao Tome and Principe', + 'sv' => 'El Salvador', + 'sy' => 'Syria', + 'sz' => 'Swaziland', + 'tc' => 'Turks and Caicos Islands', + 'td' => 'Chad', + 'tf' => 'French Southern Territories', + 'tg' => 'Togo', + 'th' => 'Thailand', + 'tj' => 'Tajikistan', + 'tk' => 'Tokelau', + 'tm' => 'Turkmenistan', + 'tn' => 'Tunisia', + 'to' => 'Tonga', + 'tp' => 'East Timor', + 'tr' => 'Turkey', + 'tt' => 'Trinidad, Tobago', + 'tv' => 'Tuvalu', + 'tw' => 'Taiwan', + 'tz' => 'Tanzania', + 'ua' => 'Ukraine', + 'ug' => 'Uganda', + 'uk' => 'United Kingdom', + 'um' => 'United States Minor Islands', + 'us' => 'United States of America', + 'uy' => 'Uruguay', + 'uz' => 'Uzbekistan', + 'va' => 'Vatican City', + 'vc' => 'Saint Vincent, Grenadines', + 've' => 'Venezuela', + 'vg' => 'Virgin Islands (British)', + 'vi' => 'Virgin Islands (USA)', + 'vn' => 'Viet Nam', + 'vu' => 'Vanuatu', + 'wf' => 'Wallis and Futuna Islands', + 'ws' => 'Samoa', + 'ye' => 'Yemen', + 'yt' => 'Mayotte', + 'yu' => 'Yugoslavia', + 'za' => 'South Africa', + 'zm' => 'Zambia', + 'zr' => 'Zaire', + 'zw' => 'Zimbabwe'); + + public static function getCountries() + { + return self::$countries; + } + + public function validate($value) + { + $value = strtolower($value); + return isset(self::$countries[$value]); + } +} \ No newline at end of file diff --git a/ipf/orm/validator/creditcard.php b/ipf/orm/validator/creditcard.php new file mode 100644 index 0000000..f4a59f6 --- /dev/null +++ b/ipf/orm/validator/creditcard.php @@ -0,0 +1,45 @@ + 'visa', + "/^5[12345]\d{14}$/" => 'mastercard', + "/^3[47]\d{13}$/" => 'amex', + "/^6011\d{12}$/" => 'discover', + "/^30[012345]\d{11}$/" => 'diners', + "/^3[68]\d{12}$/" => 'diners', + ); + foreach ($card_regexes as $regex => $type) { + if (preg_match($regex, $value)) { + $cardType = $type; + break; + } + } + if ( ! $cardType) { + return false; + } + /* mod 10 checksum algorithm */ + $revcode = strrev($value); + $checksum = 0; + for ($i = 0; $i < strlen($revcode); $i++) { + $currentNum = intval($revcode[$i]); + if ($i & 1) { /* Odd position */ + $currentNum *= 2; + } + /* Split digits and add. */ + $checksum += $currentNum % 10; + if ($currentNum > 9) { + $checksum += 1; + } + } + if ($checksum % 10 == 0) { + return true; + } else { + return false; + } + } +} \ No newline at end of file diff --git a/ipf/orm/validator/date.php b/ipf/orm/validator/date.php new file mode 100644 index 0000000..b0916a9 --- /dev/null +++ b/ipf/orm/validator/date.php @@ -0,0 +1,17 @@ +_args[$arg])) { + return $this->_args[$arg]; + } + return null; + } + + public function __isset($arg) + { + return isset($this->_args[$arg]); + } + + public function __set($arg, $value) + { + $this->_args[$arg] = $value; + + return $this; + } + + public function getArg($arg) + { + if ( ! isset($this->_args[$arg])) { + throw new IPF_ORM_Exception_Validator('Unknown option ' . $arg); + } + return $this->_args[$arg]; + } + + public function setArg($arg, $value) + { + $this->_args[$arg] = $value; + + return $this; + } + + public function getArgs() + { + return $this->_args; + } +} \ No newline at end of file diff --git a/ipf/orm/validator/email.php b/ipf/orm/validator/email.php new file mode 100644 index 0000000..6e07505 --- /dev/null +++ b/ipf/orm/validator/email.php @@ -0,0 +1,11 @@ +_className = $className; + } + + public function add($invalidFieldName, $errorCode = 'general') + { + // FIXME: In the future the error stack should contain nothing but validator objects + if (is_object($errorCode) && strpos(get_class($errorCode), 'IPF_ORM_Validator_') !== false) { + $validator = $errorCode; + $this->_validators[$invalidFieldName][] = $validator; + $className = get_class($errorCode); + $errorCode = strtolower(substr($className, strlen('IPF_ORM_Validator_'), strlen($className))); + } + + $this->_errors[$invalidFieldName][] = $errorCode; + } + + public function remove($fieldName) + { + if (isset($this->_errors[$fieldName])) { + unset($this->_errors[$fieldName]); + } + } + + public function get($fieldName) + { + return isset($this->_errors[$fieldName]) ? $this->_errors[$fieldName] : null; + } + + public function set($fieldName, $errorCode) + { + $this->add($fieldName, $errorCode); + } + + public function contains($fieldName) + { + return array_key_exists($fieldName, $this->_errors); + } + + public function clear() + { + $this->_errors = array(); + } + + public function getIterator() + { + return new ArrayIterator($this->_errors); + } + + public function toArray() + { + return $this->_errors; + } + + public function count() + { + return count($this->_errors); + } + + public function getClassname() + { + return $this->_className; + } + + public function getValidators() + { + return $this->_validators; + } +} \ No newline at end of file diff --git a/ipf/orm/validator/future.php b/ipf/orm/validator/future.php new file mode 100644 index 0000000..b075f38 --- /dev/null +++ b/ipf/orm/validator/future.php @@ -0,0 +1,43 @@ +args) && isset($this->args['timezone'])) { + switch (strtolower($this->args['timezone'])) { + case 'gmt': + $now = gmdate("U") - date("Z"); + break; + default: + $now = getdate(); + break; + } + } else { + $now = getdate(); + } + + if ($now['year'] > $e[0]) { + return false; + } else if ($now['year'] == $e[0]) { + if ($now['mon'] > $e[1]) { + return false; + } else if ($now['mon'] == $e[1]) { + return $now['mday'] < $e[2]; + } else { + return true; + } + } else { + return true; + } + } +} \ No newline at end of file diff --git a/ipf/orm/validator/htmlcolor.php b/ipf/orm/validator/htmlcolor.php new file mode 100644 index 0000000..59cf78d --- /dev/null +++ b/ipf/orm/validator/htmlcolor.php @@ -0,0 +1,12 @@ +args) && strlen($value) < $this->args) { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/ipf/orm/validator/nospace.php b/ipf/orm/validator/nospace.php new file mode 100644 index 0000000..b9f326c --- /dev/null +++ b/ipf/orm/validator/nospace.php @@ -0,0 +1,9 @@ +args) && isset($this->args['timezone'])) { + switch (strtolower($this->args['timezone'])) { + case 'gmt': + $now = gmdate("U") - date("Z"); + break; + default: + $now = getdate(); + break; + } + } else { + $now = getdate(); + } + + if ($now['year'] < $e[0]) { + return false; + } else if ($now['year'] == $e[0]) { + if ($now['mon'] < $e[1]) { + return false; + } else if ($now['mon'] == $e[1]) { + return $now['mday'] > $e[2]; + } else { + return true; + } + } else { + return true; + } + } +} \ No newline at end of file diff --git a/ipf/orm/validator/range.php b/ipf/orm/validator/range.php new file mode 100644 index 0000000..2a895d3 --- /dev/null +++ b/ipf/orm/validator/range.php @@ -0,0 +1,15 @@ +args[0]) && $value < $this->args[0]) { + return false; + } + if (isset($this->args[1]) && $value > $this->args[1]) { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/ipf/orm/validator/readonly.php b/ipf/orm/validator/readonly.php new file mode 100644 index 0000000..dac58f8 --- /dev/null +++ b/ipf/orm/validator/readonly.php @@ -0,0 +1,10 @@ +invoker->getModified(); + return array_key_exists($this->field, $modified) ? false : true; + } +} diff --git a/ipf/orm/validator/regexp.php b/ipf/orm/validator/regexp.php new file mode 100644 index 0000000..3981f13 --- /dev/null +++ b/ipf/orm/validator/regexp.php @@ -0,0 +1,25 @@ +args)) { + return true; + } + if (is_array($this->args)) { + foreach ($this->args as $regexp) { + if ( ! preg_match($regexp, $value)) { + return false; + } + } + return true; + } else { + if (preg_match($this->args, $value)) { + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/ipf/orm/validator/time.php b/ipf/orm/validator/time.php new file mode 100644 index 0000000..7c90fd4 --- /dev/null +++ b/ipf/orm/validator/time.php @@ -0,0 +1,27 @@ += 0 && $hr <= 23 && $min >= 0 && $min <= 59 && $sec >= 0 && $sec <= 59; + } +} \ No newline at end of file diff --git a/ipf/orm/validator/timestamp.php b/ipf/orm/validator/timestamp.php new file mode 100644 index 0000000..b214308 --- /dev/null +++ b/ipf/orm/validator/timestamp.php @@ -0,0 +1,30 @@ +validate($date)) { + return false; + } + + if ( ! $timeValidator->validate($time)) { + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/ipf/orm/validator/unique.php b/ipf/orm/validator/unique.php new file mode 100644 index 0000000..8f03404 --- /dev/null +++ b/ipf/orm/validator/unique.php @@ -0,0 +1,35 @@ +invoker->getTable(); + $pks = $table->getIdentifier(); + + if ( is_array($pks) ) { + $pks = join(',', $pks); + } + + $sql = 'SELECT ' . $pks . ' FROM ' . $table->getTableName() . ' WHERE ' . $this->field . ' = ?'; + + $values = array(); + $values[] = $value; + + // If the record is not new we need to add primary key checks because its ok if the + // unique value already exists in the database IF the record in the database is the same + // as the one that is validated here. + $state = $this->invoker->state(); + if ( ! ($state == IPF_ORM_Record::STATE_TDIRTY || $state == IPF_ORM_Record::STATE_TCLEAN)) { + foreach ((array) $table->getIdentifier() as $pk) { + $sql .= " AND {$pk} != ?"; + $values[] = $this->invoker->$pk; + } + } + + $stmt = $table->getConnection()->getDbh()->prepare($sql); + $stmt->execute($values); + + return ( ! is_array($stmt->fetch())); + } +} \ No newline at end of file diff --git a/ipf/orm/validator/unsigned.php b/ipf/orm/validator/unsigned.php new file mode 100644 index 0000000..7f6cf1e --- /dev/null +++ b/ipf/orm/validator/unsigned.php @@ -0,0 +1,14 @@ + true, + 'AL' => true, + 'AR' => true, + 'AZ' => true, + 'CA' => true, + 'CO' => true, + 'CT' => true, + 'DC' => true, + 'DE' => true, + 'FL' => true, + 'GA' => true, + 'HI' => true, + 'IA' => true, + 'ID' => true, + 'IL' => true, + 'IN' => true, + 'KS' => true, + 'KY' => true, + 'LA' => true, + 'MA' => true, + 'MD' => true, + 'ME' => true, + 'MI' => true, + 'MN' => true, + 'MO' => true, + 'MS' => true, + 'MT' => true, + 'NC' => true, + 'ND' => true, + 'NE' => true, + 'NH' => true, + 'NJ' => true, + 'NM' => true, + 'NV' => true, + 'NY' => true, + 'OH' => true, + 'OK' => true, + 'OR' => true, + 'PA' => true, + 'PR' => true, + 'RI' => true, + 'SC' => true, + 'SD' => true, + 'TN' => true, + 'TX' => true, + 'UT' => true, + 'VA' => true, + 'VI' => true, + 'VT' => true, + 'WA' => true, + 'WI' => true, + 'WV' => true, + 'WY' => true + ); + public function getStates() + { + return self::$states; + } + + public function validate($value) + { + return isset(self::$states[$value]); + } +} \ No newline at end of file diff --git a/ipf/project.php b/ipf/project.php new file mode 100644 index 0000000..932c290 --- /dev/null +++ b/ipf/project.php @@ -0,0 +1,94 @@ +apps[$appname] = null; + } + } + + private function __clone(){ + } + + private function appClassName($name){ + return $name.'_App'; + } + + public function appList(){ + return $this->apps; + } + + // Lazy Application Loader + public function getApp($name){ + if (!array_key_exists($name,$this->apps)) + throw new IPF_Exception_Panic("Application \"$name\" not found"); + if ($this->apps[$name]==null){ + $className = $this->appClassName($name); + $this->apps[$name] = new $className(); + } + return $this->apps[$name]; + } + + public function checkApps(){ + foreach( $this->apps as $appname=>&$app) + $this->getApp($appname); + } + + public function generateModels(){ + foreach( $this->apps as $appname=>&$app) + $this->getApp($appname)->generateModels(); + } + + public function createTablesFromModels(){ + foreach( $this->apps as $appname=>&$app) + $this->getApp($appname)->createTablesFromModels(); + } + + public function generateSql(){ + $sql = ''; + foreach( $this->apps as $appname=>&$app) + $sql .= $this->getApp($appname)->generateSql(); + $sql .= "\n\n"; + return $sql; + } + + private function loadModels(){ + foreach( $this->apps as $appname=>&$app){ + $this->getApp($appname)->loadModels(); + } + } + + private function cli(){ + $cli = new IPF_Cli(); + $cli->run(); + } + + public function run(){ + $dsn = IPF::get('dsn'); + if ($dsn=='') + throw new IPF_Exception_Panic('Specify dsn in config file'); + IPF_ORM_Manager::connection($dsn); + $this->loadModels(); + if (php_sapi_name()=='cli'){ + $this->cli(); + return; + } + IPF_ORM_Manager::getInstance()->setAttribute(IPF_ORM::ATTR_VALIDATE, IPF_ORM::VALIDATE_ALL); + $this->router = new IPF_Router(); + $this->router->dispatch(IPF_HTTP_URL::getAction()); + } +} \ No newline at end of file diff --git a/ipf/router.php b/ipf/router.php new file mode 100644 index 0000000..a8c7a4f --- /dev/null +++ b/ipf/router.php @@ -0,0 +1,82 @@ +processRequest($req); + if ($response !== false) { + // $response is a response + $response->render($req->method != 'HEAD' and !defined('IN_UNIT_TESTS')); + $skip = true; + break; + } + } + } + if ($skip === false) { + $response = IPF_Router::match($req); + if (!empty($req->response_vary_on)) { + $response->headers['Vary'] = $req->response_vary_on; + } + $middleware = array_reverse($middleware); + foreach ($middleware as $mw) { + if (method_exists($mw, 'processResponse')) { + $response = $mw->processResponse($req, $response); + } + } + //var_dump($response); + $response->render($req->method != 'HEAD'); + } + return array($req, $response); + } + + public static function match($req) + { + foreach (IPF::get('urls') as $url) { + $prefix = $url['prefix']; + foreach ($url['urls'] as $suburl){ + $regex = $prefix.$suburl['regex']; + //print "Regex = $regex
    "; + //print "Query = {$req->query}
    "; + $match = array(); + if (preg_match($regex, $req->query, $match)) { + try{ + IPF::loadFunction($suburl['func']); + $r = $suburl['func']($req, $match); + if (!is_a($r,'IPF_HTTP_Response')) + return new IPF_HTTP_Response_ServerError('function '.$suburl['func'].'() must return IPF_HTTP_Response instance'); + return $r; + } catch (IPF_Exception $e) { + return new IPF_HTTP_Response_ServerErrorDebug($e); + } + /* + try{ + IPF::loadFunction($suburl['func']); + return $suburl['func']($req, $match); + } catch (IPF_HTTP_Error404 $e) { + // Need to add a 404 error handler + // something like IPF::get('404_handler', 'class::method') + } catch (Exception $e) { + if (IPF::get('debug', false) == true) { + return new IPF_HTTP_Response_ServerErrorDebug($e); + } else { + return new IPF_HTTP_Response_ServerError($e); + } + } + */ + } + } + } + return new IPF_HTTP_Response_NotFound(); + } +} + diff --git a/ipf/session/app.php b/ipf/session/app.php new file mode 100644 index 0000000..e1046a5 --- /dev/null +++ b/ipf/session/app.php @@ -0,0 +1,9 @@ +array('Session') + )); + } +} \ No newline at end of file diff --git a/ipf/session/middleware.php b/ipf/session/middleware.php new file mode 100644 index 0000000..f61928c --- /dev/null +++ b/ipf/session/middleware.php @@ -0,0 +1,96 @@ +COOKIE[IPF::get('session_cookie_id')])) { + $request->user = $user; + $request->session = $session; + return false; + } + try { + $data = $this->_decodeData($request->COOKIE[IPF::get('session_cookie_id')]); + } catch (Exception $e) { + $request->user = $user; + $request->session = $session; + return false; + } + if (isset($data[$user->session_key])) { + $found_user = $user->getTable()->find($data[$user->session_key]); + if ($found_user->id) { + $request->user = $found_user; + if (43200 < IPF_Utils::dateCompare($request->user->last_login)) { + $request->user->last_login = gmdate('Y-m-d H:i:s'); + $request->user->save(); + } + } else + $request->user = $user; + } else + $request->user = $user; + + if (isset($data['IPF_SESSION_KEY'])) { + $found_session = $session->getTable()->findOneBySession_key($data['IPF_SESSION_KEY']); + if ($found_session) + $request->session = $found_session; + else + $request->session = $session; + } else { + $request->session = $session; + } + return false; + } + + function processResponse($request, $response) + { + if ($request->session->touched) { + $request->session->save(); + $data = array(); + if ($request->user->id > 0) { + $data[$request->user->session_key] = $request->user->id; + } + $data['IPF_SESSION_KEY'] = $request->session->session_key; + $response->cookies[IPF::get('session_cookie_id')] = $this->_encodeData($data); + } + return $response; + } + + protected function _encodeData($data) + { + if ('' == ($key = IPF::get('secret_key'))) { + throw new IPF_Exception('Security error: "secret_key" is not set in the configuration file.'); + } + $data = serialize($data); + return base64_encode($data).md5(base64_encode($data).$key); + } + + protected function _decodeData($encoded_data) + { + $check = substr($encoded_data, -32); + $base64_data = substr($encoded_data, 0, strlen($encoded_data)-32); + if (md5($base64_data.IPF::get('secret_key')) != $check) { + throw new IPF_Exception('The session data may have been tampered.'); + } + return unserialize(base64_decode($base64_data)); + } + + /* + public static function processContext($signal, &$params) + { + $params['context'] = array_merge($params['context'], + IPF_Session_Middleware_ContextPreProcessor($params['request'])); + } + */ +} + +/* +function IPF_Session_Middleware_ContextPreProcessor($request) +{ + return array('user' => $request->user); +} + +Pluf_Signal::connect('Pluf_Template_Context_Request::construct', + array('Pluf_Middleware_Session', 'processContext')); +*/ \ No newline at end of file diff --git a/ipf/session/models.yml b/ipf/session/models.yml new file mode 100644 index 0000000..7574c05 --- /dev/null +++ b/ipf/session/models.yml @@ -0,0 +1,9 @@ +--- +Session: + columns: + session_key: + primary: true + type: string(40) + session_data: string + expire_data: timestamp + diff --git a/ipf/session/models/Session.php b/ipf/session/models/Session.php new file mode 100644 index 0000000..bfdb11d --- /dev/null +++ b/ipf/session/models/Session.php @@ -0,0 +1,47 @@ +data = array(); + $this->touched = true; + } + + function setData($key, $value=null){ + if (is_null($value)) { + unset($this->data[$key]); + } else { + $this->data[$key] = $value; + } + $this->touched = true; + } + + function getData($key=null, $default=''){ + if (is_null($key)) + return parent::getData(); + + if (isset($this->data[$key])) { + return $this->data[$key]; + } else { + return $default; + } + } + + function getNewSessionKey() + { + $key = md5(microtime().rand(0, 123456789).rand(0, 123456789).IPF::get('secret_key')); + return $key; + } + + function preSave($event) + { + $this->session_data = serialize($this->data); + if ($this->session_key == '') { + $this->session_key = $this->getNewSessionKey(); + } + $this->expire_data = gmdate('Y-m-d H:i:s', time()+31536000); + } +} \ No newline at end of file diff --git a/ipf/session/models/_generated/BaseSession.php b/ipf/session/models/_generated/BaseSession.php new file mode 100644 index 0000000..741b93d --- /dev/null +++ b/ipf/session/models/_generated/BaseSession.php @@ -0,0 +1,16 @@ +setTableName('session'); + $this->hasColumn('session_key', 'string', 40, array('primary' => true, 'type' => 'string', 'length' => '40')); + $this->hasColumn('session_data', 'string', null, array('type' => 'string')); + $this->hasColumn('expire_data', 'timestamp', null, array('type' => 'timestamp')); + } + +} \ No newline at end of file diff --git a/ipf/shortcuts.php b/ipf/shortcuts.php new file mode 100644 index 0000000..27ddb69 --- /dev/null +++ b/ipf/shortcuts.php @@ -0,0 +1,30 @@ + 0 && $item->id == $id) { + return $item; + } + throw new IPF_HTTP_Error404(); + } + + static function RenderToResponse($tplfile, $params, $request=null) + { + $tmpl = new IPF_Template($tplfile); + if (is_null($request)) { + $context = new IPF_Template_Context($params); + } else { + $context = new IPF_Template_Context_Request($request, $params); + } + return new IPF_HTTP_Response($tmpl->render($context)); + } + + static function GetFormForModel($model, $data=null, $extra=array(), $label_suffix=null) + { + $extra['model'] = $model; + return new IPF_Form_Model($data, $extra, $label_suffix); + } +} \ No newline at end of file diff --git a/ipf/template.php b/ipf/template.php new file mode 100644 index 0000000..f15f505 --- /dev/null +++ b/ipf/template.php @@ -0,0 +1,120 @@ +tpl = $template; + if (is_null($folders)) { + $this->folders = IPF::get('template_dirs'); + } else { + $this->folders = $folders; + } + if (is_null($cache)) { + $this->cache = IPF::get('tmp'); + } else { + $this->cache = $cache; + } + + } + + function render($c=null) + { + $this->compiled_template = $this->getCompiledTemplateName(); + if (!file_exists($this->compiled_template) or IPF::get('debug')) { + $compiler = new IPF_Template_Compiler($this->tpl, $this->folders); + $this->template_content = $compiler->getCompiledTemplate(); + $this->write(); + } + if (is_null($c)) { + $c = new IPF_Template_Context(); + } + $this->context = $c; + ob_start(); + $t = $c; + try { + include $this->compiled_template; + } catch (Exception $e) { + ob_clean(); + throw $e; + } + $a = ob_get_contents(); + ob_end_clean(); + return $a; + } + + function getCompiledTemplateName() + { + $_tmp = var_export($this->folders, true); + return $this->cache.'/IPF_Template-'.md5($_tmp.$this->tpl).'.phps'; + } + + function write() + { + $fp = @fopen($this->compiled_template, 'a'); + if ($fp !== false) { + flock($fp, LOCK_EX); + ftruncate($fp, 0); + rewind($fp); + fwrite($fp, $this->template_content, strlen($this->template_content)); + flock($fp, LOCK_UN); + fclose($fp); + @chmod($this->compiled_template, 0777); + return true; + } else { + throw new IPF_Exception_Template(sprintf(__('Cannot write the compiled template: %s'), $this->compiled_template)); + } + return false; + } + +} + +function IPF_Template_unsafe($string) +{ + return new IPF_Template_SafeString($string, true); +} + +function IPF_Template_htmlspecialchars($string) +{ + return htmlspecialchars((string)$string, ENT_COMPAT, 'UTF-8'); +} + +function IPF_Template_dateFormat($date, $format='%b %e, %Y') +{ + if (substr(PHP_OS,0,3) == 'WIN') { + $_win_from = array ('%e', '%T', '%D'); + $_win_to = array ('%#d', '%H:%M:%S', '%m/%d/%y'); + $format = str_replace($_win_from, $_win_to, $format); + } + $date = date('Y-m-d H:i:s', strtotime($date.' GMT')); + return strftime($format, strtotime($date)); +} + +function IPF_Template_timeFormat($time, $format='Y-m-d H:i:s') +{ + return date($format, $time); +} + +function IPF_Template_safeEcho($mixed, $echo=true) +{ + if (!is_object($mixed) or 'IPF_Template_SafeString' !== get_class($mixed)) { + if ($echo) { + echo htmlspecialchars((string) $mixed, ENT_COMPAT, 'UTF-8'); + } else { + return htmlspecialchars((string) $mixed, ENT_COMPAT, 'UTF-8'); + } + } else { + if ($echo) { + echo $mixed->value; + } else { + return $mixed->value; + } + } +} diff --git a/ipf/template/compiler.php b/ipf/template/compiler.php new file mode 100644 index 0000000..935faac --- /dev/null +++ b/ipf/template/compiler.php @@ -0,0 +1,481 @@ + 'strtoupper', + 'lower' => 'strtolower', + 'escxml' => 'htmlspecialchars', + 'escape' => 'IPF_Template_htmlspecialchars', + 'strip_tags' => 'strip_tags', + 'escurl' => 'rawurlencode', + 'capitalize' => 'ucwords', + // Not var_export because of recursive issues. + 'debug' => 'print_r', + 'fulldebug' => 'var_export', + 'count' => 'count', + 'nl2br' => 'nl2br', + 'trim' => 'trim', + 'unsafe' => 'IPF_Template_unsafe', + 'safe' => 'IPF_Template_unsafe', + 'date' => 'IPF_Template_dateFormat', + 'time' => 'IPF_Template_timeFormat', + ); + + public $_usedModifiers = array(); + + protected $_allowedTags = array( + 'url' => 'IPF_Template_Tag_Url', + ); + protected $_extraTags = array(); + + protected $_blockStack = array(); + + protected $_transStack = array(); + protected $_transPlural = false; + + protected $_sourceFile; + + protected $_currentTag; + + public $templateFolders = array(); + + public $templateContent = ''; + + public $_extendBlocks = array(); + + public $_extendedTemplate = ''; + + function __construct($template_file, $folders=array(), $load=true) + { + $allowedtags = IPF::get('template_tags', array()); + $this->_allowedTags = array_merge($allowedtags, $this->_allowedTags); + $modifiers = IPF::get('template_modifiers', array()); + $this->_modifier = array_merge($modifiers, $this->_modifier); + + foreach ($this->_allowedTags as $name=>$model) { + $this->_extraTags[$name] = new $model(); + } + $this->_sourceFile = $template_file; + $this->_allowedInVar = array_merge($this->_vartype, $this->_op); + $this->_allowedInExpr = array_merge($this->_vartype, $this->_op); + $this->_allowedAssign = array_merge($this->_vartype, $this->_assignOp, + $this->_op); + $this->templateFolders = $folders; + if ($load) { + $this->loadTemplateFile($template_file); + } + } + + function compile() + { + $this->compileBlocks(); + $tplcontent = $this->templateContent; + $tplcontent = preg_replace('!{\*(.*?)\*}!s', '', $tplcontent); + $tplcontent = preg_replace('!<\?php(.*?)\?>!s', '', $tplcontent); + preg_match_all('!{literal}(.*?){/literal}!s', $tplcontent, $_match); + $this->_literals = $_match[1]; + $tplcontent = preg_replace("!{literal}(.*?){/literal}!s", '{literal}', $tplcontent); + // Core regex to parse the template + $result = preg_replace_callback('/{((.).*?)}/s', + array($this, '_callback'), + $tplcontent); + if (count($this->_blockStack)) { + trigger_error(sprintf(__('End tag of a block missing: %s'), end($this->_blockStack)), E_USER_ERROR); + } + return $result; + } + + function getCompiledTemplate() + { + $result = $this->compile(); + if (count($this->_usedModifiers)) { + $code = array(); + foreach ($this->_usedModifiers as $modifier) { + $code[] = 'IPF::loadFunction(\''.$modifier.'\'); '; + } + $result = ''.$result; + } + $result = str_replace(array('?>', ''), '', $result); + $result = str_replace("?>\n", "?>\n\n", $result); + return $result; + } + + function compileBlocks() + { + $tplcontent = $this->templateContent; + $this->_extendedTemplate = ''; + // Match extends on the first line of the template + if (preg_match("!{extends\s['\"](.*?)['\"]}!", $tplcontent, $_match)) { + $this->_extendedTemplate = $_match[1]; + } + // Get the blocks in the current template + $cnt = preg_match_all("!{block\s(\S+?)}(.*?){/block}!s", $tplcontent, $_match); + // Compile the blocks + for ($i=0; $i<$cnt; $i++) { + if (!isset($this->_extendBlocks[$_match[1][$i]]) + or false !== strpos($this->_extendBlocks[$_match[1][$i]], '~~{~~superblock~~}~~')) { + $compiler = clone($this); + $compiler->templateContent = $_match[2][$i]; + $_tmp = $compiler->compile(); + $this->updateModifierStack($compiler); + if (!isset($this->_extendBlocks[$_match[1][$i]])) { + $this->_extendBlocks[$_match[1][$i]] = $_tmp; + } else { + $this->_extendBlocks[$_match[1][$i]] = str_replace('~~{~~superblock~~}~~', $_tmp, $this->_extendBlocks[$_match[1][$i]]); + } + } + } + if (strlen($this->_extendedTemplate) > 0) { + // The template of interest is now the extended template + // as we are not in a base template + $this->loadTemplateFile($this->_extendedTemplate); + $this->_sourceFile = $this->_extendedTemplate; + $this->compileBlocks(); //It will recurse to the base template. + } else { + // Replace the current blocks by a place holder + if ($cnt) { + $this->templateContent = preg_replace("!{block\s(\S+?)}(.*?){/block}!s", "{block $1}", $tplcontent, -1); + } + } + } + + function loadTemplateFile($file) + { + // FIXME: Very small security check, could be better. + if (strpos($file, '..') !== false) { + throw new IPF_Exception(sprintf(__('Template file contains invalid characters: %s'), $file)); + } + foreach ($this->templateFolders as $folder) { + if (file_exists($folder.'/'.$file)) { + $this->templateContent = file_get_contents($folder.'/'.$file); + return; + } + } + // File not found in all the folders. + throw new IPF_Exception(sprintf(__('Template file not found: %s'), $file)); + } + + function _callback($matches) + { + list(,$tag, $firstcar) = $matches; + if (!preg_match('/^\$|[\'"]|[a-zA-Z\/]$/', $firstcar)) { + trigger_error(sprintf(__('Invalid tag syntax: %s'), $tag), E_USER_ERROR); + return ''; + } + $this->_currentTag = $tag; + if (in_array($firstcar, array('$', '\'', '"'))) { + if ('blocktrans' !== end($this->_blockStack)) { + return '_parseVariable($tag).'); ?>'; + } else { + $tok = explode('|', $tag); + $this->_transStack[substr($tok[0], 1)] = $this->_parseVariable($tag); + return '%%'.substr($tok[0], 1).'%%'; + } + } else { + if (!preg_match('/^(\/?[a-zA-Z0-9_]+)(?:(?:\s+(.*))|(?:\((.*)\)))?$/', $tag, $m)) { + trigger_error(sprintf(__('Invalid function syntax: %s'), $tag), E_USER_ERROR); + return ''; + } + if (count($m) == 4){ + $m[2] = $m[3]; + } + if (!isset($m[2])) $m[2] = ''; + if($m[1] == 'ldelim') return '{'; + if($m[1] == 'rdelim') return '}'; + if ($m[1] != 'include') { + return '_parseFunction($m[1], $m[2]).'?>'; + } else { + return $this->_parseFunction($m[1], $m[2]); + } + } + } + + function _parseVariable($expr) + { + $tok = explode('|', $expr); + $res = $this->_parseFinal(array_shift($tok), $this->_allowedInVar); + foreach ($tok as $modifier) { + if (!preg_match('/^(\w+)(?:\:(.*))?$/', $modifier, $m)) { + trigger_error(sprintf(__('Invalid modifier syntax: (%s) %s'), $this->_currentTag, $modifier), E_USER_ERROR); + return ''; + } + $targs = array($res); + if(isset($m[2])){ + $res = $this->_modifier[$m[1]].'('.$res.','.$m[2].')'; + } + else if (isset($this->_modifier[$m[1]])) { + $res = $this->_modifier[$m[1]].'('.$res.')'; + } else { + trigger_error(sprintf(__('Unknown modifier: (%s) %s'), $this->_currentTag, $m[1]), E_USER_ERROR); + return ''; + } + if (!in_array($this->_modifier[$m[1]], $this->_usedModifiers)) { + $this->_usedModifiers[] = $this->_modifier[$m[1]]; + } + } + return $res; + } + + function _parseFunction($name, $args) + { + switch ($name) { + case 'if': + $res = 'if ('.$this->_parseFinal($args, $this->_allowedInExpr).'): '; + array_push($this->_blockStack, 'if'); + break; + case 'else': + if (end($this->_blockStack) != 'if') { + trigger_error(sprintf(__('End tag of a block missing: %s'), end($this->_blockStack)), E_USER_ERROR); + } + $res = 'else: '; + break; + case 'elseif': + if (end($this->_blockStack) != 'if') { + trigger_error(sprintf(__('End tag of a block missing: %s'), end($this->_blockStack)), E_USER_ERROR); + } + $res = 'elseif('.$this->_parseFinal($args, $this->_allowedInExpr).'):'; + break; + case 'foreach': + $res = 'foreach ('.$this->_parseFinal($args, array(T_AS, T_DOUBLE_ARROW, T_STRING, T_OBJECT_OPERATOR), array(';','!')).'): '; + array_push($this->_blockStack, 'foreach'); + break; + case 'while': + $res = 'while('.$this->_parseFinal($args,$this->_allowedInExpr).'):'; + array_push($this->_blockStack, 'while'); + break; + case '/foreach': + case '/if': + case '/while': + $short = substr($name,1); + if(end($this->_blockStack) != $short){ + trigger_error(sprintf(__('End tag of a block missing: %s'), end($this->_blockStack)), E_USER_ERROR); + } + array_pop($this->_blockStack); + $res = 'end'.$short.'; '; + break; + case 'assign': + $res = $this->_parseFinal($args, $this->_allowedAssign).'; '; + break; + case 'literal': + if(count($this->_literals)){ + $res = '?>'.array_shift($this->_literals).''.$this->_extendBlocks[$args].'~~{~~superblock~~}~~_parseFinal($args, $this->_allowedAssign); + $res = 'echo(__('.$argfct.'));'; + break; + case 'blocktrans': + array_push($this->_blockStack, 'blocktrans'); + $res = ''; + $this->_transStack = array(); + if ($args) { + $this->_transPlural = true; + $_args = $this->_parseFinal($args, $this->_allowedAssign, + array(';', '[', ']'), true); + $res .= '$_b_t_c='.trim(array_shift($_args)).'; '; + } + $res .= 'ob_start(); '; + break; + case '/blocktrans': + $short = substr($name,1); + if(end($this->_blockStack) != $short){ + trigger_error(sprintf(__('End tag of a block missing: %s'), end($this->_blockStack)), E_USER_ERROR); + } + $res = ''; + if ($this->_transPlural) { + $res .= '$_b_t_p=ob_get_contents(); ob_end_clean(); echo('; + $res .= 'IPF_Translation::sprintf(_n($_b_t_s, $_b_t_p, $_b_t_c), array('; + $_tmp = array(); + foreach ($this->_transStack as $key=>$_trans) { + $_tmp[] = '\''.addslashes($key).'\' => IPF_Template_safeEcho('.$_trans.', false)'; + } + $res .= implode(', ', $_tmp); + unset($_trans, $_tmp); + $res .= ')));'; + $this->_transStack = array(); + } else { + $res .= '$_b_t_s=ob_get_contents(); ob_end_clean(); '; + if (count($this->_transStack) == 0) { + $res .= 'echo(__($_b_t_s)); '; + } else { + $res .= 'echo(IPF_Translation::sprintf(__($_b_t_s), array('; + $_tmp = array(); + foreach ($this->_transStack as $key=>$_trans) { + $_tmp[] = '\''.addslashes($key).'\' => IPF_Template_safeEcho('.$_trans.', false)'; + } + $res .= implode(', ', $_tmp); + unset($_trans, $_tmp); + $res .= '))); '; + $this->_transStack = array(); + } + } + $this->_transPlural = false; + array_pop($this->_blockStack); + break; + case 'plural': + $res = '$_b_t_s=ob_get_contents(); ob_end_clean(); ob_start(); '; + break; + case 'include': + // XXX fixme: Will need some security check, when online editing. + $argfct = preg_replace('!^[\'"](.*)[\'"]$!', '$1', $args); + $_comp = new IPF_Template_Compiler($argfct, $this->templateFolders); + $res = $_comp->compile(); + $this->updateModifierStack($_comp); + break; + default: + $_end = false; + $oname = $name; + if (substr($name, 0, 1) == '/') { + $_end = true; + $name = substr($name, 1); + } + // Here we should allow custom blocks. + + // Here we start the template tag calls at the template tag + // {tag ...} is not a block, so it must be a function. + if (!isset($this->_allowedTags[$name])) { + trigger_error(sprintf(__('The function tag "%s" is not allowed.'), $name), E_USER_ERROR); + } + $argfct = $this->_parseFinal($args, $this->_allowedAssign); + // $argfct is a string that can be copy/pasted in the PHP code + // but we need the array of args. + $res = ''; + if (isset($this->_extraTags[$name])) { + if (false == $_end) { + if (method_exists($this->_extraTags[$name], 'start')) { + $res .= '$_extra_tag = IPF::factory(\''.$this->_allowedTags[$name].'\', $t); $_extra_tag->start('.$argfct.'); '; + } + if (method_exists($this->_extraTags[$name], 'genStart')) { + $res .= $this->_extraTags[$name]->genStart(); + } + } else { + if (method_exists($this->_extraTags[$name], 'end')) { + $res .= '$_extra_tag = IPF::factory(\''.$this->_allowedTags[$name].'\', $t); $_extra_tag->end('.$argfct.'); '; + } + if (method_exists($this->_extraTags[$name], 'genEnd')) { + $res .= $this->_extraTags[$name]->genEnd(); + } + } + } + if ($res == '') { + trigger_error(sprintf(__('The function tag "{%s ...}" is not supported.'), $oname), E_USER_ERROR); + } + } + return $res; + } + + function _parseFinal($string, $allowed=array(), + $exceptchar=array(';'), $getAsArray=false) + { + $tokens = token_get_all(''); + $result = ''; + $first = true; + $inDot = false; + $firstok = array_shift($tokens); + $afterAs = false; + $f_key = ''; + $f_val = ''; + $results = array(); + + // il y a un bug, parfois le premier token n'est pas T_OPEN_TAG... + if ($firstok == '<' && $tokens[0] == '?' && is_array($tokens[1]) + && $tokens[1][0] == T_STRING && $tokens[1][1] == 'php') { + array_shift($tokens); + array_shift($tokens); + } + foreach ($tokens as $tok) { + if (is_array($tok)) { + list($type, $str) = $tok; + $first = false; + if($type == T_CLOSE_TAG){ + continue; + } + if ($type == T_AS) { + $afterAs = true; + } + if ($type == T_STRING && $inDot) { + $result .= $str; + } elseif ($type == T_VARIABLE) { + $result .= '$t->_vars[\''.substr($str, 1).'\']'; + } elseif ($type == T_WHITESPACE || in_array($type, $allowed)) { + $result .= $str; + } else { + trigger_error(sprintf(__('Invalid syntax: (%s) %s.'), $this->_currentTag, $str), E_USER_ERROR); + return ''; + } + } else { + if (in_array($tok, $exceptchar)) { + trigger_error(sprintf(__('Invalid character: (%s) %s.'), $this->_currentTag, $tok), E_USER_ERROR); + } elseif ($tok == '.') { + $inDot = true; + $result .= '->'; + } elseif ($tok == '~') { + $result .= '.'; + } elseif ($tok =='[') { + $result.=$tok; + } elseif ($tok ==']') { + $result.=$tok; + } elseif ($getAsArray && $tok == ',') { + $results[]=$result; + $result=''; + } else { + $result .= $tok; + } + $first = false; + } + } + if (!$getAsArray) { + return $result; + } else { + if ($result != '') { + $results[] = $result; + } + return $results; + } + } + + protected function updateModifierStack($compiler) + { + foreach ($compiler->_usedModifiers as $_um) { + if (!in_array($_um, $this->_usedModifiers)) { + $this->_usedModifiers[] = $_um; + } + } + } +} + diff --git a/ipf/template/context.php b/ipf/template/context.php new file mode 100644 index 0000000..53818e3 --- /dev/null +++ b/ipf/template/context.php @@ -0,0 +1,24 @@ +_vars = new IPF_Template_ContextVars($vars); + } + + function get($var) + { + if (isset($this->_vars[$var])) { + return $this->_vars[$var]; + } + return ''; + } + + function set($var, $value) + { + $this->_vars[$var] = $value; + } +} diff --git a/ipf/template/context/request.php b/ipf/template/context/request.php new file mode 100644 index 0000000..0074620 --- /dev/null +++ b/ipf/template/context/request.php @@ -0,0 +1,20 @@ + $request), $vars); + foreach (IPF::get('template_context_processors', array()) as $proc) { + IPF::loadFunction($proc); + $vars = array_merge($proc($request), $vars); + } + $params = array('request' => $request, + 'context' => $vars); + + //IPF_Signal::send('IPF_Template_Context_Request::construct', + // 'IPF_Template_Context_Request', $params); + $this->_vars = new IPF_Template_ContextVars($params['context']); + } +} + diff --git a/ipf/template/contextvars.php b/ipf/template/contextvars.php new file mode 100644 index 0000000..637c12e --- /dev/null +++ b/ipf/template/contextvars.php @@ -0,0 +1,26 @@ +offsetExists($prop)) { + return ''; + } + return parent::offsetGet($prop); + } + + function __get($prop) + { + if (isset($this->$prop)) { + return $this->$prop; + } else { + return $this->offsetGet($prop); + } + } + + function __toString() + { + return var_export($this, true); + } +} diff --git a/ipf/template/safestring.php b/ipf/template/safestring.php new file mode 100644 index 0000000..b8f76a7 --- /dev/null +++ b/ipf/template/safestring.php @@ -0,0 +1,25 @@ +value = $mixed->value; + } else { + $this->value = ($safe) ? $mixed : htmlspecialchars($mixed, ENT_COMPAT, 'UTF-8'); + } + } + + function __toString() + { + return $this->value; + } + + public static function markSafe($string) + { + return new IPF_Template_SafeString($string, true); + } +} \ No newline at end of file diff --git a/ipf/template/tag.php b/ipf/template/tag.php new file mode 100644 index 0000000..ed98a33 --- /dev/null +++ b/ipf/template/tag.php @@ -0,0 +1,10 @@ +context = $context; + } +} diff --git a/ipf/template/tag/url.php b/ipf/template/tag/url.php new file mode 100644 index 0000000..1f75995 --- /dev/null +++ b/ipf/template/tag/url.php @@ -0,0 +1,11 @@ +$max_length) ) + return false; + if ( is_numeric($s[0])) + return false; + if (!preg_match('/^[a-zA-Z0-9_]+$/', $s )) + return false; + return true; + } + + public static function isEmail($value){ + $qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]'; + $dtext = '[^\\x0d\\x5b-\\x5d\\x80-\\xff]'; + $atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+'; + $quotedPair = '\\x5c[\\x00-\\x7f]'; + $domainLiteral = "\\x5b($dtext|$quotedPair)*\\x5d"; + $quotedString = "\\x22($qtext|$quotedPair)*\\x22"; + $domain_ref = $atom; + $subDomain = "($domain_ref|$domainLiteral)"; + $word = "($atom|$quotedString)"; + $domain = "$subDomain(\\x2e$subDomain)+"; + $localPart = "$word(\\x2e$word)*"; + $addrSpec = "$localPart\\x40$domain"; + return (bool) preg_match("!^$addrSpec$!D", $value); + } + + static function prettySize($size) + { + $mb = 1024*1024; + if ($size > $mb) { + $mysize = sprintf('%01.2f', $size/$mb).' '. __('MB'); + } elseif ($size >= 1024) { + $mysize = sprintf('%01.2f', $size/1024).' '.__('KB'); + } else { + $mysize = sprintf('%01.2f', $size/1024).' '.__('bytes'); + } + return $mysize; + } + + static function cleanFileName($name) + { + $name = mb_strtolower($name, 'UTF-8'); + return mb_ereg_replace("/\015\012|\015|\012|\s|[^A-Za-z0-9\.\-\_]/", '_', $name); + } + + static function isValidUrl($url) + { + $ip = '(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.' + .'(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}'; + $dom = '([a-z0-9\.\-]+)'; + return (preg_match('!^(http|https|ftp|gopher)\://('.$ip.'|'.$dom.')!i', $url)) ? true : false; + } + + static function humanTitle($s){ + return ucfirst(str_replace('_',' ',$s)); + } + + static function randomString($len=35) + { + $string = ''; + $chars = '0123456789abcdefghijklmnopqrstuvwxyz' + .'ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()+=-_}{[]>appList() as $app){ + foreach($app->modelList() as $m){ + if ($model==$m) + return strtolower($app->getLabel()); + } + } + return ''; + } +} + diff --git a/ipf/version.php b/ipf/version.php new file mode 100644 index 0000000..920d2a2 --- /dev/null +++ b/ipf/version.php @@ -0,0 +1,5 @@ +