How-would-you-do-this question, long, bear with me...
# cfml-general
d
How-would-you-do-this question, long, bear with me here... Big app, complex, written over many years by many people, semi-old school -- pages are cfms, which mostly call cfc methods to get, save, and validate data. Too big to refactor into, for instance, ColdBox, which we're not using. I've got that classic request, "make another area just like this one only different". At the db level, records for both old and new areas will be in the same main table. There's a new typeID column, which distinguishes oldArea records from newArea ones. All saves use the right one, and all searches etc filter on that, so each area sees only its own records. At the model level, I'm thinking to create new cfcs as needed, which extend the existing ones, and override any methods that need to act differently in the new area. Examples are getPageTitle(), getSearchFormFields(), getFieldsToShow(), validate(), etc. They also set this.typeID, which is used in all those main table get and save queries, as above. My question is, what does the cfm level of this look like, the pages users actually go to? Those pages need to call the newArea version of all model cfcs, where the behaviors that are different for the two areas are implemented. Don't want to clone and modify those pages for the new area if I can help it. I don't want a URL param everywhere saying which area this is either, so how does this work? Main pages in the existing area are form, history, index, search, summary, view. My initial thought was to create newArea versions of those pages, which just set request.isNewArea (not really, but conceptually), and call the corresponding oldArea page. A bit of futzing around in newArea's application.cfc I think lets all those newArea files just be blank; they have to exist to not 404, then application.cfc sets the needed request vars and includes the corresponding oldArea file. All model method calls need to be to the appropriate model, newArea or oldArea. I'm thinking that application.cfc will also set request.modelPath to point to it, which the cfms will use. This also requires updating all links in oldArea files, and any files they call, to be relative to the current path, /oldArea/ or /newArea/. So, if you've made it through this thought experiment/sketch of a plan, congratulations and thanks! What do you think? Too Rube Goldberg? Should I just clone all the cfms and modify them, like I said I didn't want to do, to keep everything simple and straightforward? Any other ideas?
w
at the start of the request in app.cfc do some simple regex matching against the requested url and set some variables in whatever scope (request probably) you'll key off of to determine which cfcs/methods/whatever you need.
this is pretty well trodden ground of course but maybe it doesn't feel like it?
so if it's 'area' driven, you can do it like this without having to explicitly wail on every file
g
You might also consider using AOP and onMissingMethod().
a
rewriting links can be done at the webserver level so you can move in stages
I'm not sure why you need to extend the existing CFCs though? Your new app can just have a mapping to the existing CFCs directory can't it?
e
Supporting some ultra legacy code that was written by JJ Alarie, or at least back when Coldfusion was an Alarie product - Here is what I have done, or at least works for me: 1. Copy the source page, store it someplace safe outside the application directory. 2. Break out as much of the code, as one can to template files for that page, place them in a directory for templates/lib/thatpage/ to templates/ that group/thatpage/ 3. cfinclube the functions back into the protype replacement page and test for errors, 4. Make note of the functions, or cfcode needed for that page in your IDE, or what ever you use (so you can call it back later) As you continue doing this you will note that some code can be replaced with functions and components, and you can replace the template code with proper components. Once you hit a milestone where everything is broken down to templates and components, then you can start to work on implementing code optimizations and further modernizing the code.
d
Thanks folks. Re why extend the existing CFCs, it's not because of path issues, it's because a bunch of methods will probably be the same for old and new, but a bunch won't. For instance, getFieldsToShow() will diverge a lot, so getRequiredFields() will too, etc. That matches my sense of what extends is for -- inherit what's common, override what's unique to you. I prefer that to forking within each method -- if (request.areaName == "oldArea") doOldStuff() else doNewStuff(). I'm thinking app.cfc will set request.model to an instance of or the path to one or the other CFC, and all calls will be made to that. Make sense? I'll respond to some other bits in a little bit.
šŸ‘ 1
a
well composition is preferred to inheritance - for the very case where you are having to override a method - but equally I get this is an stepping stone so that could be valid as a pragmatic step
we have a model which is shared by I think 3 different apps - one is ColdBox, one if FW1, the other is spaghetti (the original), but all the model code is the same. The work I had to do was to use Wirebox in all three apps though so that the model works the same for all apps. The framework doesn't really matter then
e
I have to agree with @Dave Merrill on not extending legacy CFCs. it just leads to unexpected errors.
d
@Evil Ware I'm not following your last post. I'm proposing extending the existing CFCs, which I don't consider to be "legacy". They're reasonably well structured, with documented and validated inputs, and they return the data they're expected to, which is used by the current oldArea. The only reason I see for new ones is to accommodate the differences between oldArea and newArea. Is there something else that makes them "legacy", i.e., undesirable?
@websolete re the actual URLs. I'm reluctant to have URL rewrites define application behavior. This app runs on several dev sites, stage/customer preview, and production. Our IIS config is pretty generic, and not under source control. I'd prefer application behavior be defined in code. All that is just to say that the files in newArea have to actually exist to avoid 404s, unless I want to dispatch this sub-application through an error handler, which, no. If they exist, do they contain actual code modified for newArea? I was thinking no, don't clone and fork, they just call their counterparts in oldArea, which respond to the request vars set in app.cfc to create the new behaviors. I'm not 100% sure that will turn out to be workable, need to do some testing.
e
I would still sick with the above, as without knowing every bit of code, seemingly working changes usually breaks something down the line.
d
@Evil Ware Which "above" do you mean I should stick to?
e
As for URL chaging app behavior, it depends if it's based on you setting the URL and the rendered content or if someone can just ask for a dynamic URL parameter and get the content they want. You can always create a simple router code, which is the focal point of all content, then pass the correct content based upon what they request.
@Dave Merrill I would stick to the mapped plan of rewriting the code, as you will understand how "it" works and will be able to better create the requested changes. Template it out so even if its not using a framework the next person or you, after a long night of drinking can at 4 am look at the path, look at the file and the error code and go , oh its "" which I changed on XYZ.
d
In other words, clone the directory of CFMs and modify the for newArea? Maybe. (I don't drink šŸ˜‰)
w
@Dave Merrill i wasn't suggesting rewrites by the webserver, i was suggesting 'routing' via application.cfc's lifecycle methods. also to clarify, what is the difference between oldArea and newArea functionality wise? do they render the same data but from different datasources? you talk of cloning the cfm pages into the newArea which suggests they do render the same basic data
d
@websolete Sorry, I thought you meant URL rewrites to serve oldArea and newArea with the same cfms. The plan I outlined included app.cfc code to set request vars based on which area you're in, which I think is what you meant too. That's what lead me to say that there need to be files in /newArea/ that correspond to the ones in /oldArea/, so they're not 404s. Those files can maybe be empty, with app.cfc's onRequest() method including the corresponding oldArea file, after setting those request vars to modify behavior. Make sense? Re oldArea/newArea differences, the underlying difference is that two different kinds of pretty similar but not identical items will be stored in the same table of the same datasource. The oldArea looks at one of them, newArea at the other, never both. There are a variety of behavior differences between oldArea and newArea, for example: • Different columns shown in list, detail, and edit views • Different labels for some columns in each view • Different required fields • Different links shown in some views • Different choice list items available for some radio or select fields Make sense? Again, I very much appreciate anyone who's following this and commenting, I know it's not a two sentence deal to get your head around.
w
IF the structure of this legacy app is 'the url is simply the directory path to the file from the root' then i might suggest in newArea that you do NOT clone the cfms and instead implement a 'view loader' via a custom tag. picture this: you sniff and set vars in app.cfc as required, and if it's a newArea request you'd grab the requested path and pass that into /newArea/index.cfm where in that cfm it cfmodule's the original requested file path (or cfincludes the target file into the custom tag). since you'll be inside a custom tag you'll get nice variable scope isolation, and have a lot of flexibility on compositing the final view output. this way, you don't have to clone the old cfm templates and can hopefully handle data differences required for newArea more gracefully without duplicating templates
the custom tag is optional but i recommend it. you can always just cfinclude from onRequest and prevent the original requested url from being loaded
d
My as-yet-unproven-or-finalized theory was to use app.cfc as the "view loader", but the idea's the same. I don't need to clone the oldArea files, but as I said, I do need at least an empty file for each one in newArea, or requests there 404 before they even get to my code. This may need some thought, since there are a number of files in a subdirectory of oldArea that are ajax targets, only. I'll think about whether a cfmodule layer as part of the view loader strategy would be beneficial.
w
you should not have to duplicate the URIs by having actual files in those locations (empty or not). at worst, you'd need only one to route the request to which then loads whatever it needs.
requested: /newArea/subdir/whatever.cfm loaded: /newArea/index.cfm which modules or cfincludes /oldArea/subdir/whatever.cfm
anyway, not telling you anything you don't know, but i've used this model with good success in similar scenarios
d
@websolete RE
you should not have to duplicate the URIs by having actual files in those locations (empty or not)
, app.cfc in newArea has an onRequest() handler, which calcs the path to the corresponding oldArea file and includes it. That's working fine, but only if the corresponding file in newArea actually exists. It can be empty, but without it it's 404 before it gets to my code. That's what I'd expect, but you seemed to be saying those newArea files aren't needed. How are you thinking that would work without them? By changing the URLs to something like this, more a front controller model: /newArea/index.cfm?page=targetPageInOldArea ?
w
you may need a single webserver rewrite to handle those non-existent target urls, so a rewrite from
/newArea/path/to/file
rewritten to
/newArea/index.cfm?path=path/to/file
may be required (or
/newArea/index.cfm/path/to/file
), yes, but you still won't need a newArea empty file for every single one
šŸ‘ 1
but once you've got that in place, all the other routing/logic you can do in code
b
Great description, @Dave Merrill. I think your description contains its own answer. At least, I found an answer in your description. Namely: (1) apply inheritance to the old-model CFCs where necessary, resulting in corresponding new-model CFCs; (2) only write those new CFM pages that pertain to the changes and new functionality that the new-model CFCs contain. ======= My reference ======= SITUATION - THE APP: Big app, complex, written over many years by many people, semi-old school -- pages are cfms, which mostly call cfc methods to get, save, and validate data. Too big to refactor into, for instance, ColdBox, which we're not using. REQUIREMENTS: "make another area just like this one only different". Don't want to clone and modify those pages for the new area if I can help it. I don't want a URL param everywhere saying which area this is either, so how does this work? DATABASE: records for both old and new areas will be in the same main table. There's a new typeID column, which distinguishes oldArea records from newArea ones. All saves use the right one, and all searches etc filter on that, so each area sees only its own records. MODEL: I'm thinking to create new cfcs as needed, which extend the existing ones, and override any methods that need to act differently in the new area. Examples are getPageTitle(), getSearchFormFields(), getFieldsToShow(), validate(), etc. They also set this.typeID, which is used in all those main table get and save queries, as above. IMPLEMENTATION QUESTION: What does the cfm level of this look like, the pages users actually go to? IMPLEMENTATION ANSWER: Those pages need to call the newArea version of all model cfcs, where the behaviors that are different for the two areas are implemented.
d
Thanks for making it through that @bkbk, and for your comments. FYI, this is deployed to the preview server now, with one small gnarly bit not done. I'm meeting with my main customer contact late this afternoon to go through it with her, I'm not expecting any surprises. For the cfms, I created new directories in the two places they were needed, with app.cfc setting some request vars for which type it is, its friendly name, etc. I originally did the path calculations in app.cfc to include the corresponding old cfm, but since that needed empty cfms to not 404, I just had the new cfms explicitly include the old ones. That seemed clearer and less weird, and it's also a pattern this app already used in the old version of this area, so When In Rome... I also created a new model cfc that extends the original, populating this.itemTypeID from the request var, in both the new and old ones. The overrides in the new one are pretty minimal and pretty clear. I added a hasFeature() method there that cfms can use to ask if specific UI features should show here. All of that has worked out well. So that's the clean parts. For various reasons, I did end up with code in some of the cfms that shows different stuff depending on the value of that request var. Pushing all of that into model methods started to feel like I was being Correct and verbose instead of doing what came naturally. I'm pretty comfortable with how it turned out, all told. So far. We'll see what I and any potential successors think of all this in 5 years :)